diff --git a/.doctrees/deployment.doctree b/.doctrees/deployment.doctree index d2ffd26..6675cb5 100644 Binary files a/.doctrees/deployment.doctree and b/.doctrees/deployment.doctree differ diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle index 942eed0..53efde2 100644 Binary files a/.doctrees/environment.pickle and b/.doctrees/environment.pickle differ diff --git a/.doctrees/quickstart.doctree b/.doctrees/quickstart.doctree index 1c77766..1e297e3 100644 Binary files a/.doctrees/quickstart.doctree and b/.doctrees/quickstart.doctree differ diff --git a/.doctrees/testing.doctree b/.doctrees/testing.doctree index 1c91d71..e8ca6e2 100644 Binary files a/.doctrees/testing.doctree and b/.doctrees/testing.doctree differ diff --git a/.doctrees/tour.doctree b/.doctrees/tour.doctree index a1ee73a..05defef 100644 Binary files a/.doctrees/tour.doctree and b/.doctrees/tour.doctree differ diff --git a/_sources/deployment.rst.txt b/_sources/deployment.rst.txt index f30b59c..7644ceb 100644 --- a/_sources/deployment.rst.txt +++ b/_sources/deployment.rst.txt @@ -1,34 +1,32 @@ Deployment ========== -Responder applications are standard ASGI apps. You can deploy them anywhere -you'd deploy a Python web service. +Responder applications are standard `ASGI `_ +apps. ASGI (Asynchronous Server Gateway Interface) is the modern successor +to WSGI — it supports async, WebSockets, and HTTP/2. This means you can +deploy a Responder app anywhere that runs Python, using any ASGI server. Running Locally --------------- -The simplest way to run your application:: - - # api.py - import responder - - api = responder.API() - - @api.route("/") - def hello(req, resp): - resp.text = "hello, world!" +During development, ``api.run()`` is all you need:: if __name__ == "__main__": api.run() -This starts a production uvicorn server on ``127.0.0.1:5042``. +This starts a `uvicorn `_ server on +``127.0.0.1:5042``. Uvicorn is a lightning-fast ASGI server built on +`uvloop `_ — it handles thousands of +concurrent connections efficiently and protects against slowloris attacks, +making a reverse proxy like nginx optional for many deployments. Docker ------ -A minimal Dockerfile for deploying a Responder application:: +Docker is the most common way to package and deploy web applications. +Here's a minimal Dockerfile:: FROM python:3.13-slim WORKDIR /app @@ -43,44 +41,63 @@ Build and run:: $ docker build -t myapi . $ 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. + Cloud Platforms --------------- -Responder automatically honors the ``PORT`` environment variable, which is -set by most cloud platforms. When ``PORT`` is set, Responder binds to -``0.0.0.0`` on that port automatically. +Responder automatically honors the ``PORT`` environment variable. When +``PORT`` is set, the server binds to ``0.0.0.0`` on that port — this is +the convention that virtually every cloud platform uses. -This works out of the box with: +This means zero configuration on: -- **Fly.io** -- **Railway** -- **Render** -- **Google Cloud Run** -- **Azure Container Apps** -- **AWS App Runner** +- **Fly.io** — ``fly launch`` and you're done +- **Railway** — push your code, Railway sets ``PORT`` +- **Render** — set start command to ``python api.py`` +- **Google Cloud Run** — containerize and deploy +- **Azure Container Apps** — same pattern +- **AWS App Runner** — and here too -Just deploy your code and set the start command to ``python api.py``. +The pattern is always the same: deploy your code, set the start command +to ``python api.py``, and the platform handles the rest. Uvicorn Directly ---------------- -For more control over the production server, you can bypass ``api.run()`` -and use uvicorn directly:: +For production deployments where you want more control, bypass +``api.run()`` and use uvicorn directly:: $ uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4 -This gives you access to all of uvicorn's options: worker count, SSL -certificates, access logging, and more. See the -`uvicorn documentation `_ for details. +The ``--workers`` flag spawns multiple processes, each handling requests +independently. A good starting point is 2-4 workers per CPU core. + +Uvicorn supports many options — SSL certificates, access logging, graceful +shutdown timeouts, and more. See the +`uvicorn documentation `_ for details. Reverse Proxy ------------- -In production, you may want to place Responder behind a reverse proxy like -nginx or Caddy for SSL termination, load balancing, or serving static assets. +For high-traffic production deployments, you may want a reverse proxy like +`nginx `_ or `Caddy `_ in +front of your application for: + +- **SSL/TLS termination** — let the proxy handle HTTPS certificates +- **Load balancing** — distribute traffic across multiple app instances +- **Static asset serving** — offload static files to the proxy +- **Rate limiting** — at the infrastructure level Responder's ``TrustedHostMiddleware`` and ``HTTPSRedirectMiddleware`` work -correctly behind proxies that set standard forwarding headers. +correctly behind proxies that set standard forwarding headers +(``X-Forwarded-For``, ``X-Forwarded-Proto``). + +That said, uvicorn is production-ready on its own. Many applications run +uvicorn directly without a reverse proxy and do just fine. diff --git a/_sources/quickstart.rst.txt b/_sources/quickstart.rst.txt index f675fc3..1781eed 100644 --- a/_sources/quickstart.rst.txt +++ b/_sources/quickstart.rst.txt @@ -2,164 +2,229 @@ Quick Start =========== This guide will walk you through the basics of building a web service with -Responder. By the end, you'll know how to declare routes, handle requests, -send responses, render templates, and process background tasks. +Responder. By the end, you'll understand how HTTP requests and responses +work, how to define routes, read data from clients, send data back, render +HTML templates, and process work in the background. Create a Web Service -------------------- -The first thing you need to do is declare a web service. This is the central -object that holds all your routes, middleware, and configuration:: +Every web application starts with a single object — the application +instance. In Responder, this is the ``API`` class. It holds your routes, +middleware, templates, and configuration. Think of it as the central +nervous system of your web service:: import responder api = responder.API() +That's it. One import, one line. You now have a fully functional ASGI +application with gzip compression, static file serving, session support, +and a production-ready server — all wired up and ready to go. + Hello World ----------- -Next, add a route. Here, we'll make the root URL say "hello, world!":: +A web service isn't very useful until it can respond to requests. In HTTP, +a *route* maps a URL path to a function that handles it. When a client +(like a browser or ``curl``) sends a request to that path, your function +runs and produces a response. + +Here's the simplest possible route:: @api.route("/") def hello_world(req, resp): resp.text = "hello, world!" -Every view receives a ``req`` (request) and ``resp`` (response) object. You -don't need to return anything — just mutate the response directly. +Two things to notice: + +1. Every view function receives two arguments: ``req`` (the incoming + request) and ``resp`` (the outgoing response). +2. You don't return anything. Instead, you *mutate* the response object + directly. This is a deliberate design choice — it keeps the API + consistent whether you're setting text, JSON, headers, cookies, or + status codes. Run the Server -------------- -Start your web service with ``api.run()``:: +Start your web service with a single call:: api.run() -This spins up a production-grade uvicorn server on port ``5042``, ready for -incoming HTTP requests. +This spins up a production-grade `uvicorn `_ +server on port ``5042``, ready for incoming HTTP requests. Open +``http://localhost:5042`` in your browser and you'll see your hello world +response. You can customize the port with ``api.run(port=8000)``. The ``PORT`` environment variable is also honored automatically — when set, Responder -binds to ``0.0.0.0`` on that port, which is what cloud platforms like -Fly.io, Railway, and Google Cloud Run expect. +binds to ``0.0.0.0`` on that port, which is what cloud platforms expect. .. note:: Both sync and async views are supported. The ``async`` keyword is always - optional — use it when you need to ``await`` something. + optional — use it when you need to ``await`` something, like reading a + request body or querying a database. Route Parameters ---------------- -If you want dynamic URLs, use Python's familiar f-string syntax to declare -variables in your routes:: +Static URLs like ``/about`` are useful, but most applications need dynamic +routes — URLs that contain variable data, like a user ID or a product slug. + +In Responder, you declare route parameters using Python's f-string syntax:: @api.route("/hello/{who}") def hello_to(req, resp, *, who): resp.text = f"hello, {who}!" A ``GET`` request to ``/hello/world`` will respond with ``hello, world!``. +A request to ``/hello/guido`` will respond with ``hello, guido!``. -Route parameters are passed as keyword-only arguments (after the ``*``). +Route parameters are passed as *keyword-only* arguments (after the ``*`` +in the function signature). This is a Python feature that makes the +interface explicit — you always know which arguments come from the URL. Type Convertors ^^^^^^^^^^^^^^^ -You can constrain route parameters to specific types. The parameter will be -automatically converted before it reaches your view:: +By default, route parameters are strings. But often you want them as +integers, UUIDs, or other types. Responder can convert them automatically +using type annotations in the route pattern:: @api.route("/add/{a:int}/{b:int}") async def add(req, resp, *, a, b): resp.text = f"{a} + {b} = {a + b}" +Here, ``a`` and ``b`` will arrive as Python ``int`` objects, not strings. +If someone requests ``/add/3/hello``, they'll get a 404 — the route won't +match because ``hello`` isn't a valid integer. + Supported types: -- ``str`` — matches any string without slashes (default) -- ``int`` — matches digits, converts to ``int`` -- ``float`` — matches decimal numbers, converts to ``float`` +- ``str`` — matches any string without slashes (this is the default) +- ``int`` — matches digits and converts to ``int`` +- ``float`` — matches decimal numbers and converts to ``float`` - ``uuid`` — matches UUID strings like ``550e8400-e29b-41d4-a716-446655440000`` - ``path`` — matches any string *including* slashes, useful for file paths + like ``/files/{filepath:path}`` Sending Responses ----------------- -Responder gives you several ways to send data back to the client. Just set -the appropriate property on the response object. +When an HTTP server receives a request, it must send back a response. Every +HTTP response has three parts: a status code (like ``200 OK`` or ``404 Not +Found``), headers (metadata like ``Content-Type``), and a body (the actual +data). -**Text and HTML**:: +Responder lets you set all three by mutating the response object. + +**Text and HTML** — the simplest response types. ``resp.text`` sets the +``Content-Type`` to ``text/plain``, while ``resp.html`` sets it to +``text/html``:: resp.text = "plain text response" resp.html = "

HTML response

" -**JSON** — the most common pattern for APIs. Set ``resp.media`` to any -JSON-serializable Python object:: +**JSON** — the lingua franca of web APIs. Set ``resp.media`` to any +JSON-serializable Python object — a dict, a list, whatever — and Responder +will serialize it to JSON and set the right headers:: @api.route("/hello/{who}/json") def hello_json(req, resp, *, who): resp.media = {"hello": who} If the client sends an ``Accept: application/x-yaml`` header, the same data -will be returned as YAML instead. Content negotiation is automatic. +will be returned as YAML instead. This is called *content negotiation* — +the server and client agree on a format. It happens automatically. -**Files** — serve a file from disk with automatic content-type detection:: +**Files** — serve a file from disk. Responder uses Python's ``mimetypes`` +module to figure out the ``Content-Type`` from the file extension:: resp.file("reports/annual.pdf") -**Raw bytes**:: +**Raw bytes** — for binary data like images or protocol buffers:: resp.content = b"\x89PNG\r\n..." -**Status codes and headers**:: +**Status codes** — HTTP status codes tell the client what happened. ``200`` +means success, ``201`` means something was created, ``404`` means not found, +``500`` means the server broke. Set it directly:: resp.status_code = 201 + +**Headers** — HTTP headers carry metadata. Common ones include +``Content-Type``, ``Cache-Control``, ``Authorization``, and custom +application headers:: + resp.headers["X-Custom"] = "value" -**Redirects**:: +**Redirects** — tell the client to go somewhere else:: api.redirect(resp, location="/new-url") +This sends a ``301 Moved Permanently`` response by default. The client's +browser will automatically follow the redirect. + Reading Requests ---------------- -The request object gives you access to everything the client sent. +The other half of HTTP is the request — the data the client sends to your +server. This includes the HTTP method (GET, POST, PUT, DELETE), the URL, +headers, query parameters, cookies, and optionally a body. -**Method and URL**:: +Responder wraps all of this in the ``req`` object. + +**Method and URL** — every HTTP request has a method (what the client wants +to do) and a URL (what resource it's about):: req.method # "get", "post", etc. (lowercase) req.full_url # "http://example.com/path?q=1" req.url # parsed URL object -**Headers** — case-insensitive, just like you'd expect:: +**Headers** — HTTP headers carry metadata from the client, like what +content types it accepts, authentication tokens, and more. Responder's +headers dict is case-insensitive, because the HTTP spec says header names +are case-insensitive:: req.headers["Content-Type"] req.headers["content-type"] # same thing -**Query parameters**:: +**Query parameters** — the part of the URL after the ``?``. These are +commonly used for search, filtering, and pagination:: # GET /search?q=python&page=2 req.params["q"] # "python" req.params["page"] # "2" -**Path parameters** — also available on the request object:: +Note that query parameters are always strings. If you need an integer, +you'll need to convert it yourself: ``int(req.params["page"])``. + +**Path parameters** — the dynamic parts of the URL that matched your route +pattern. These are also available on the request object, which is useful +in before-request hooks where they aren't passed as function arguments:: req.path_params["user_id"] # same as the keyword argument -**Request body** — for POST/PUT/PATCH requests, you need to ``await`` the -body content:: +**Request body** — for POST, PUT, and PATCH requests, the client sends +data in the body. Since reading the body is an I/O operation, you need to +``await`` it:: - # JSON body + # JSON body (the most common format for APIs) data = await req.media() - # Form data + # Form data (from HTML forms) data = await req.media("form") - # File uploads + # File uploads (multipart) files = await req.media("files") # Raw bytes @@ -170,41 +235,59 @@ body content:: **Other useful properties**:: - req.is_json # True if content type is JSON - req.cookies # dict of cookies - req.session # session data (dict) - req.client # (host, port) tuple - req.is_secure # True if HTTPS + req.is_json # True if the content type is JSON + req.cookies # dict of cookies sent by the client + req.session # session data (a signed, server-side dict) + req.client # (host, port) tuple — the client's IP address + req.is_secure # True if the request came over HTTPS Rendering Templates ------------------- -Responder includes built-in `Jinja2 `_ -support. Templates are loaded from the ``templates/`` directory by default. +While APIs typically return JSON, many web applications need to render +HTML pages. Responder includes built-in support for +`Jinja2 `_, one of the most popular +templating engines in the Python ecosystem. -The simplest way is to use ``api.template()``:: +Templates let you write HTML with placeholders that get filled in with +dynamic data. This keeps your presentation logic (HTML) separate from +your application logic (Python) — a pattern called +*separation of concerns*. + +The simplest way to render a template is ``api.template()``. Templates +are loaded from the ``templates/`` directory by default:: @api.route("/hello/{name}/html") def hello_html(req, resp, *, name): resp.html = api.template("hello.html", name=name) -You can also use the ``Templates`` class directly for more control:: +The template file ``templates/hello.html`` might look like:: + +

Hello, {{ name }}!

+ +The ``{{ name }}`` part is a Jinja2 expression — it gets replaced with +the value you passed in. + +You can also use the ``Templates`` class directly for more control over +the template directory and configuration:: from responder.templates import Templates - templates = Templates(directory="templates") + templates = Templates(directory="my_templates") @api.route("/page") def page(req, resp): resp.html = templates.render("page.html", title="Hello") -Async rendering is supported too:: +For applications that need non-blocking template rendering (rare, but +useful under extreme load), async rendering is supported:: templates = Templates(directory="templates", enable_async=True) resp.html = await templates.render_async("page.html", title="Hello") -You can render template strings without a file:: +And for quick one-off templates, you can render a string directly without +a file:: resp.html = api.template_string("Hello, {{ name }}!", name="world") @@ -213,7 +296,13 @@ Background Tasks ---------------- Sometimes you want to accept a request, respond immediately, and do the -actual processing later. Responder makes this easy with background tasks:: +actual processing later. This is a common pattern for operations that take +a long time — sending emails, processing images, updating caches, or +calling slow external APIs. + +Responder makes this easy with background tasks. Decorate any function +with ``@api.background.task`` and it will run in a thread pool, separate +from the request/response cycle:: @api.route("/incoming") async def receive_incoming(req, resp): @@ -227,8 +316,17 @@ actual processing later. Responder makes this easy with background tasks:: process_data(data) - # Respond immediately — processing continues in the background + # This response is sent immediately, while process_data + # continues running in the background. resp.media = {"status": "accepted"} -The ``@api.background.task`` decorator wraps any function to run in a thread -pool. The client gets an immediate response while the work continues. +The client gets an instant response — the heavy lifting happens after. +This is the same pattern used by task queues like Celery, but much simpler +for lightweight use cases where you don't need a full message broker. + +.. note:: + + Background tasks run in threads, not processes. They share memory with + your application, which makes them fast to start but means CPU-intensive + work will block the event loop. For heavy computation, consider a proper + task queue. diff --git a/_sources/testing.rst.txt b/_sources/testing.rst.txt index 0e926af..6271d2d 100644 --- a/_sources/testing.rst.txt +++ b/_sources/testing.rst.txt @@ -279,7 +279,7 @@ Tips ``API()`` configuration (like ``cors=True``), create a new instance in the test rather than sharing the fixture. -- **Use ``api.url_for()``** instead of hard-coded paths. It's a small +- Use ``api.url_for()`` instead of hard-coded paths. It's a small thing, but it makes refactoring painless. - **Test the contract, not the implementation.** Assert on status codes, diff --git a/_sources/tour.rst.txt b/_sources/tour.rst.txt index deef200..962c720 100644 --- a/_sources/tour.rst.txt +++ b/_sources/tour.rst.txt @@ -1,15 +1,27 @@ Feature Tour ============ -This section walks through Responder's features in detail. Each section -includes working code examples you can copy into your application. +This section walks through Responder's features in depth. Each section +explains the concept, shows working code, and explains the design choices +behind it. If you're new to web development, this is a good place to learn +how modern web frameworks work under the hood. Method Filtering ---------------- -By default, a route matches all HTTP methods. If you want to restrict a -route to specific methods, pass the ``methods`` parameter:: +HTTP defines several *methods* (also called verbs) that describe what a +client wants to do with a resource. The most common are: + +- ``GET`` — retrieve data +- ``POST`` — create something new +- ``PUT`` — replace something entirely +- ``PATCH`` — update part of something +- ``DELETE`` — remove something + +By default, a Responder route matches all methods. This is fine for simple +endpoints, but REST APIs typically map different methods to different +operations. Use the ``methods`` parameter to restrict a route:: @api.route("/items", methods=["GET"]) def list_items(req, resp): @@ -20,15 +32,22 @@ route to specific methods, pass the ``methods`` parameter:: data = await req.media() resp.media = {"created": data} -Note the ``check_existing=False`` — this allows you to register multiple -handlers for the same path with different methods. +Note the ``check_existing=False`` — Responder normally prevents you from +registering two routes with the same path (to catch typos). When you +intentionally want multiple handlers for the same path with different +methods, you need to opt in. Class-Based Views ----------------- -For more complex resources, you can use class-based views. Responder will -dispatch to the appropriate method handler based on the HTTP method:: +Function-based views are great for simple endpoints, but sometimes you want +to group related HTTP methods together into a single resource. This is +where class-based views come in — a pattern popularized by +`Falcon `_. + +Responder dispatches to the appropriate method handler based on the HTTP +method:: @api.route("/{greeting}") class GreetingResource: @@ -47,14 +66,19 @@ middleware scoped to a single route. Method-specific handlers (``on_get``, ``on_post``, ``on_put``, ``on_delete``, etc.) are called after. No inheritance required — just define a class with the right method names. +This is simpler than Django's ``View`` classes and more Pythonic than +framework-specific base classes. Lifespan Events --------------- -Modern applications often need to set up resources on startup (database -connections, caches, ML models) and tear them down on shutdown. Responder -supports the lifespan context manager pattern:: +Real applications need to set up resources when they start (database +connection pools, ML models, caches) and tear them down when they stop. +This is called the application *lifespan*. + +The modern approach is the *context manager* pattern, where startup and +shutdown are two halves of the same block:: from contextlib import asynccontextmanager @@ -68,7 +92,11 @@ supports the lifespan context manager pattern:: api = responder.API(lifespan=lifespan) -You can also use the traditional event decorator style:: +Everything before ``yield`` runs at startup. Everything after runs at +shutdown. If startup fails, the server won't start. If shutdown raises, +it's logged but the server still exits. + +The traditional event decorator style also works:: @api.on_event("startup") async def startup(): @@ -78,57 +106,71 @@ You can also use the traditional event decorator style:: async def shutdown(): print("shutting down") -The context manager approach is preferred for new code — it makes the -startup/shutdown relationship explicit and keeps related code together. +The context manager is preferred for new code — it keeps related startup +and shutdown logic together and makes resource cleanup more explicit. Serving Files ------------- -Serve files from disk with automatic content-type detection. Responder -uses Python's ``mimetypes`` module to figure out the right ``Content-Type`` -header for you:: +Web applications often need to serve files — downloads, reports, images. +Responder makes this simple with ``resp.file()``, which reads a file from +disk and sets the ``Content-Type`` header automatically using Python's +``mimetypes`` module:: @api.route("/download") def download(req, resp): resp.file("reports/annual.pdf") -You can override the content type if needed:: +You can override the content type if the automatic detection isn't right:: @api.route("/image") def image(req, resp): resp.file("photos/cat.jpg", content_type="image/jpeg") +For large files, use ``resp.stream_file()`` to avoid loading the entire +file into memory. This streams the file in chunks:: + + @api.route("/export") + def export(req, resp): + resp.stream_file("data/export.csv") + Custom Error Handling --------------------- -By default, unhandled exceptions result in a 500 Internal Server Error. -You can register custom handlers for specific exception types to return -structured error responses:: +In production, you don't want your users to see raw Python tracebacks. +Responder lets you register custom handlers for specific exception types, +so you can return clean, structured error responses:: @api.exception_handler(ValueError) async def handle_value_error(req, resp, exc): resp.status_code = 400 resp.media = {"error": str(exc)} -Now, any route that raises a ``ValueError`` will return a clean 400 response -with a JSON error message instead of a generic 500 page. +Now, any route that raises a ``ValueError`` will return a clean JSON +response with a 400 status code instead of a generic 500 error page. + +This is a common pattern in API development — you define your own exception +classes for different error conditions, register handlers for each, and +your API always returns consistent, machine-readable error responses. Before-Request Hooks -------------------- -Run code before every request. This is useful for logging, adding common -headers, or setting up per-request state:: +Sometimes you need to run the same code before every request — +authentication checks, request logging, adding common headers, or setting +up per-request state. Before-request hooks let you do this without +duplicating code in every route:: @api.route(before_request=True) def add_headers(req, resp): - resp.headers["X-API-Version"] = "3.1" + resp.headers["X-API-Version"] = "3.2" -**Short-circuiting:** If your hook sets ``resp.status_code``, the route -handler will be skipped entirely and the response will be sent immediately. -This is the pattern for authentication guards:: +**Short-circuiting** is the really powerful part. If your hook sets +``resp.status_code``, the route handler is skipped entirely and the +response is sent immediately. This is the pattern for authentication:: @api.route(before_request=True) def auth_check(req, resp): @@ -137,19 +179,36 @@ This is the pattern for authentication guards:: resp.media = {"error": "unauthorized"} If the ``Authorization`` header is missing, the client gets a 401 response -and the actual route handler never runs. +and the actual route handler never runs. This is cleaner than adding +auth checks to every individual route. -WebSocket hooks work the same way:: - @api.before_request(websocket=True) - async def ws_auth(ws): - await ws.accept() +After-Request Hooks +------------------- + +The complement to before-request hooks. After-request hooks run after the +route handler completes but before the response is sent. They're useful +for logging, adding response headers, or any post-processing:: + + @api.after_request() + def log_response(req, resp): + print(f"{req.method} {req.full_url} -> {resp.status_code}") + + @api.after_request() + async def add_timing(req, resp): + resp.headers["X-Served-By"] = "responder" WebSocket Support ----------------- -Responder supports WebSockets for real-time, bidirectional communication:: +HTTP is a request-response protocol — the client asks, the server answers. +But some applications need real-time, bidirectional communication: chat +apps, live dashboards, multiplayer games, collaborative editors. + +`WebSockets `_ solve this by +upgrading an HTTP connection into a persistent, full-duplex channel where +both sides can send messages at any time:: @api.route("/ws", websocket=True) async def websocket(ws): @@ -161,14 +220,54 @@ Responder supports WebSockets for real-time, bidirectional communication:: You can send and receive in multiple formats: -- ``send_text`` / ``receive_text`` — plain text -- ``send_json`` / ``receive_json`` — JSON objects +- ``send_text`` / ``receive_text`` — plain text strings +- ``send_json`` / ``receive_json`` — JSON objects (auto-serialized) - ``send_bytes`` / ``receive_bytes`` — raw binary data +WebSocket routes are marked with ``websocket=True`` in the route decorator. +They receive a ``ws`` object instead of ``req`` and ``resp``. + + +Server-Sent Events (SSE) +------------------------- + +SSE is a simpler alternative to WebSockets for *one-way* real-time +communication — the server pushes events to the client, but the client +can't send messages back. This is perfect for live feeds, progress bars, +notification streams, and AI response streaming. + +Unlike WebSockets, SSE works over plain HTTP, is automatically reconnected +by the browser, and doesn't require any special client-side libraries:: + + @api.route("/events") + async def events(req, resp): + @resp.sse + async def stream(): + for i in range(10): + yield {"data": f"message {i}"} + +On the client side, you consume SSE events with JavaScript's built-in +``EventSource`` API:: + + const source = new EventSource("/events"); + source.onmessage = (event) => { + console.log(event.data); + }; + +Each yielded value can be a string (treated as data) or a dict with the +standard SSE fields:: + + yield {"event": "update", "data": "hello", "id": "1", "retry": "5000"} + yield "simple string message" + GraphQL ------- +`GraphQL `_ is a query language for APIs that lets +clients request exactly the data they need — no more, no less. Instead of +multiple REST endpoints, you define a schema and let clients query it. + Responder includes built-in GraphQL support via `Graphene `_. Set up a full GraphQL endpoint with a single method call:: @@ -183,9 +282,10 @@ with a single method call:: api.graphql("/graphql", schema=graphene.Schema(query=Query)) -Visiting ``/graphql`` in a browser renders the GraphiQL interactive IDE, -where you can explore your schema and test queries. Programmatic clients -can POST JSON queries to the same endpoint. +Visiting ``/graphql`` in a browser renders the +`GraphiQL `_ interactive IDE, where +you can explore your schema, write queries, and see results in real-time. +Programmatic clients can POST JSON queries to the same endpoint. You can access the Responder request and response objects in your resolvers through ``info.context["request"]`` and ``info.context["response"]``. @@ -194,8 +294,12 @@ through ``info.context["request"]`` and ``info.context["response"]``. OpenAPI Documentation --------------------- -Responder can generate an OpenAPI schema and serve interactive API -documentation automatically:: +`OpenAPI `_ (formerly Swagger) is the industry +standard for describing REST APIs. An OpenAPI specification lets you +auto-generate interactive documentation, client libraries, and validation +logic. + +Responder generates OpenAPI specs from your code:: api = responder.API( title="Pet Store", @@ -211,9 +315,11 @@ This gives you: There are three ways to document your endpoints. -**Pydantic models** — the recommended approach for new APIs. Use -``request_model`` and ``response_model`` to annotate your routes, and -Responder will generate the schema automatically:: +**Pydantic models** — the recommended approach. Use ``request_model`` and +``response_model`` to annotate your routes, and Responder generates the +schema automatically. When ``request_model`` is set, request bodies are +also validated automatically — invalid inputs get a ``422`` response with +detailed error messages:: from pydantic import BaseModel @@ -232,19 +338,11 @@ Responder will generate the schema automatically:: data = await req.media() resp.media = {"id": 1, **data} -This generates a full OpenAPI path with ``requestBody`` and ``responses`` -schemas, all linked by ``$ref`` to your Pydantic models in -``components/schemas``. +When ``response_model`` is set, the response is serialized through the +model — extra fields are stripped and types are enforced. -You can also register standalone schemas with the ``@api.schema`` decorator:: - - @api.schema("Pet") - class Pet(BaseModel): - name: str - age: int = 0 - -**YAML docstrings** — inline your OpenAPI spec directly in the docstring. -This gives you full control over every detail:: +**YAML docstrings** — for full control, embed OpenAPI YAML in the +docstring:: @api.route("/pets") def list_pets(req, resp): @@ -258,8 +356,7 @@ This gives you full control over every detail:: """ resp.media = [{"name": "Fido"}] -**Marshmallow schemas** — if you're already using marshmallow for -validation, Responder integrates with it via the apispec plugin:: +**Marshmallow schemas** — if you're already using marshmallow:: from marshmallow import Schema, fields @@ -267,19 +364,43 @@ validation, Responder integrates with it via the apispec plugin:: class PetSchema(Schema): name = fields.Str() -All three approaches can be mixed in the same API. Pydantic models, -marshmallow schemas, and YAML docstrings all contribute to the same -generated OpenAPI specification. +All three approaches can be mixed in the same API. You can choose from +multiple documentation themes: ``swagger_ui`` (default), ``redoc``, +``rapidoc``, or ``elements``. -You can choose from multiple documentation themes: -``swagger_ui`` (default), ``redoc``, ``rapidoc``, or ``elements``. + +Route Groups +------------ + +As your application grows, you'll want to organize routes logically. +Route groups let you share a URL prefix across related endpoints — a +common pattern for API versioning:: + + v1 = api.group("/v1") + + @v1.route("/users") + def list_users(req, resp): + resp.media = [] + + @v1.route("/users/{user_id:int}") + def get_user(req, resp, *, user_id): + resp.media = {"id": user_id} + + v2 = api.group("/v2") + + @v2.route("/users") + def list_users_v2(req, resp): + resp.media = {"users": [], "total": 0} + +This keeps your code organized without affecting the routing logic. Mounting Other Apps ------------------- -Responder can mount any WSGI or ASGI application at a subroute. This means -you can gradually migrate from Flask, or run multiple frameworks side by side:: +Responder can mount any WSGI or ASGI application at a subroute. This is +incredibly useful for gradual migrations — you can run Flask and Responder +side by side, moving routes over one at a time:: from flask import Flask @@ -293,12 +414,17 @@ you can gradually migrate from Flask, or run multiple frameworks side by side:: Requests to ``/flask/`` will be handled by Flask. Everything else goes through Responder. Both WSGI and ASGI apps are supported — Responder -wraps WSGI apps automatically. +wraps WSGI apps in an ASGI adapter automatically. Cookies ------- +`Cookies `_ are +small pieces of data that the server asks the browser to store and send +back with every subsequent request. They're the foundation of sessions, +authentication tokens, and user preferences on the web. + Reading and writing cookies is straightforward:: # Read cookies from the request @@ -307,26 +433,28 @@ Reading and writing cookies is straightforward:: # Set a cookie on the response resp.cookies["hello"] = "world" -For more control over cookie directives, use ``set_cookie``:: +For production use, you'll want to set security directives. The +``httponly`` flag prevents JavaScript from reading the cookie (defending +against XSS attacks), and ``secure`` ensures it's only sent over HTTPS:: resp.set_cookie( "token", value="abc123", - max_age=3600, - secure=True, - httponly=True, + max_age=3600, # expires in 1 hour + secure=True, # HTTPS only + httponly=True, # no JavaScript access path="/", ) -Supported directives: ``key``, ``value``, ``expires``, ``max_age``, -``domain``, ``path``, ``secure``, ``httponly``. - Cookie-Based Sessions --------------------- -Responder has built-in support for signed, cookie-based sessions. Just -read from and write to the ``session`` dictionary:: +Sessions let you store per-user data across multiple requests. Responder's +built-in sessions are cookie-based — the session data is serialized, signed +with your secret key, and stored in a cookie. The signature prevents +tampering: if someone modifies the cookie, the signature won't match and +the data will be rejected:: @api.route("/login") def login(req, resp): @@ -336,13 +464,9 @@ read from and write to the ``session`` dictionary:: def profile(req, resp): resp.media = {"user": req.session.get("username")} -The session data is stored in a cookie called ``Responder-Session``. It's -signed for tamper protection, so you can trust that the data originated -from your server. - .. warning:: - For production use, always set a secret key:: + Always set a secret key in production. The default key is not secret:: api = responder.API(secret_key="your-secret-key-here") @@ -350,141 +474,98 @@ from your server. Static Files ------------ -Static files are served from the ``static/`` directory by default:: +Most web applications serve static assets — CSS stylesheets, JavaScript +files, images, fonts. Responder serves these from the ``static/`` directory +by default:: api = responder.API(static_dir="static", static_route="/static") -Place your CSS, JavaScript, images, and other assets in the ``static/`` -directory and they'll be served automatically. +Place your assets in the ``static/`` directory and they'll be served +automatically at ``/static/style.css``, ``/static/app.js``, etc. -For single-page applications, you can serve ``index.html`` as the default -response for all unmatched routes:: +For single-page applications (React, Vue, Angular), you can serve +``index.html`` as the default response for all unmatched routes:: api.add_route("/", static=True) -You can add additional static directories at runtime:: - - api.static_app.add_directory("extra_assets") - CORS ---- -Enable Cross-Origin Resource Sharing for your API:: +`CORS `_ (Cross- +Origin Resource Sharing) is a security mechanism that controls which +websites can make requests to your API. Browsers enforce this — if your +API is at ``api.example.com`` and your frontend is at ``app.example.com``, +the browser will block requests unless your API explicitly allows it. + +Enable CORS and configure which origins are allowed:: api = responder.API(cors=True, cors_params={ - "allow_origins": ["https://example.com"], + "allow_origins": ["https://app.example.com"], "allow_methods": ["GET", "POST"], "allow_headers": ["*"], "allow_credentials": True, "max_age": 600, }) -The default CORS policy is restrictive — you must explicitly enable the -origins, methods, and headers your frontend needs. +The default policy is restrictive — you must explicitly allow each origin. +Using ``["*"]`` for allow_origins permits any website to call your API, +which is fine for public APIs but not for private ones. HSTS ---- -Force all traffic to HTTPS with a single flag:: +`HSTS `_ +(HTTP Strict Transport Security) tells browsers to always use HTTPS when +communicating with your server. Once a browser sees the HSTS header, it +will refuse to connect over plain HTTP, even if the user types ``http://`` +in the address bar:: api = responder.API(enable_hsts=True) -This adds the ``Strict-Transport-Security`` header and redirects HTTP -requests to HTTPS. - Trusted Hosts ------------- -Protect against HTTP Host header attacks by restricting which hostnames -your application will respond to:: +The ``Host`` header in an HTTP request tells the server which domain name +the client used. Attackers can forge this header to trick your application +into generating URLs to malicious domains (a class of attack called *Host +header injection*). + +Restrict which hostnames your application accepts:: api = responder.API(allowed_hosts=["example.com", "*.example.com"]) -Requests with a ``Host`` header that doesn't match any of the patterns -will receive a 400 Bad Request response. Wildcard domains are supported. - -By default, all hostnames are allowed. - - -Server-Sent Events (SSE) ------------------------- - -Stream real-time updates to the client using Server-Sent Events. This is -great for live feeds, progress updates, and AI streaming responses:: - - @api.route("/events") - async def events(req, resp): - @resp.sse - async def stream(): - for i in range(10): - yield {"data": f"message {i}"} - -Each yielded value can be a string (treated as data) or a dict with -``data``, ``event``, ``id``, and ``retry`` fields:: - - yield {"event": "update", "data": "hello", "id": "1"} - yield "simple string message" - - -Streaming Files ---------------- - -For large files, use ``resp.stream_file()`` to stream the content without -loading the entire file into memory:: - - @api.route("/download") - def download(req, resp): - resp.stream_file("large-dataset.csv") - -For small files where memory isn't a concern, ``resp.file()`` loads the -entire file at once — simpler but less efficient for large files. - - -After-Request Hooks -------------------- - -Run code after every request, useful for logging, adding headers, or -cleanup:: - - @api.after_request() - def log_response(req, resp): - print(f"{req.method} {req.full_url} -> {resp.status_code}") - - -Route Groups ------------- - -Organize related routes with a shared URL prefix. Useful for API versioning -and logical grouping:: - - v1 = api.group("/v1") - - @v1.route("/users") - def list_users(req, resp): - resp.media = [] - - @v1.route("/users/{user_id:int}") - def get_user(req, resp, *, user_id): - resp.media = {"id": user_id} +Requests with unrecognized hosts get a ``400 Bad Request``. Wildcard +patterns are supported. By default, all hostnames are allowed. Request ID ---------- -Auto-generate unique request IDs for tracing and debugging. If the client -sends an ``X-Request-ID`` header, it's forwarded; otherwise a new UUID is -generated:: +In distributed systems, tracing a single request across multiple services +is essential for debugging. Request IDs are unique identifiers attached to +each request — if something goes wrong, you can search your logs for that +ID and find every related event. + +Responder can auto-generate request IDs. If the client sends an +``X-Request-ID`` header (common in microservice architectures), it's +forwarded. Otherwise, a new UUID is generated:: api = responder.API(request_id=True) +The ID appears in the ``X-Request-ID`` response header. + Rate Limiting ------------- -Built-in token bucket rate limiter:: +Rate limiting prevents individual clients from overwhelming your API with +too many requests. It's essential for public APIs, and good practice even +for internal ones. + +Responder includes a built-in token bucket rate limiter:: from responder.ext.ratelimit import RateLimiter @@ -492,17 +573,25 @@ Built-in token bucket rate limiter:: limiter.install(api) When the limit is exceeded, clients receive a ``429 Too Many Requests`` -response with ``Retry-After`` and ``X-RateLimit-Remaining`` headers. +response with a ``Retry-After`` header. Every response includes +``X-RateLimit-Limit`` and ``X-RateLimit-Remaining`` headers so clients +can pace themselves. + +The rate limiter is per-client, keyed by IP address. MessagePack ----------- -In addition to JSON and YAML, Responder supports MessagePack for efficient -binary serialization:: +`MessagePack `_ is a binary serialization format +that's more compact and faster to parse than JSON. It's useful for +high-throughput APIs, IoT devices, and anywhere bandwidth matters. - # Decode MessagePack request body +Responder supports MessagePack alongside JSON and YAML:: + + # Decode a MessagePack request body data = await req.media("msgpack") - # Content negotiation also works — clients can send - # Accept: application/x-msgpack to receive MessagePack responses. +Content negotiation works too — clients can send +``Accept: application/x-msgpack`` to receive MessagePack responses +instead of JSON. diff --git a/deployment.html b/deployment.html index d7354eb..ca40e92 100644 --- a/deployment.html +++ b/deployment.html @@ -42,29 +42,27 @@

Deployment

-

Responder applications are standard ASGI apps. You can deploy them anywhere -you’d deploy a Python web service.

+

Responder applications are standard ASGI +apps. ASGI (Asynchronous Server Gateway Interface) is the modern successor +to WSGI — it supports async, WebSockets, and HTTP/2. This means you can +deploy a Responder app anywhere that runs Python, using any ASGI server.

Running Locally

-

The simplest way to run your application:

-
# api.py
-import responder
-
-api = responder.API()
-
-@api.route("/")
-def hello(req, resp):
-    resp.text = "hello, world!"
-
-if __name__ == "__main__":
+

During development, api.run() is all you need:

+
if __name__ == "__main__":
     api.run()
 
-

This starts a production uvicorn server on 127.0.0.1:5042.

+

This starts a uvicorn server on +127.0.0.1:5042. Uvicorn is a lightning-fast ASGI server built on +uvloop — it handles thousands of +concurrent connections efficiently and protects against slowloris attacks, +making a reverse proxy like nginx optional for many deployments.

Docker

-

A minimal Dockerfile for deploying a Responder application:

+

Docker is the most common way to package and deploy web applications. +Here’s a minimal Dockerfile:

FROM python:3.13-slim
 WORKDIR /app
 COPY . .
@@ -79,40 +77,57 @@ you’d deploy a Python web service.

$ 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.

Cloud Platforms

-

Responder automatically honors the PORT environment variable, which is -set by most cloud platforms. When PORT is set, Responder binds to -0.0.0.0 on that port automatically.

-

This works out of the box with:

+

Responder automatically honors the PORT environment variable. When +PORT is set, the server binds to 0.0.0.0 on that port — this is +the convention that virtually every cloud platform uses.

+

This means zero configuration on:

    -
  • Fly.io

  • -
  • Railway

  • -
  • Render

  • -
  • Google Cloud Run

  • -
  • Azure Container Apps

  • -
  • AWS App Runner

  • +
  • Fly.iofly launch and you’re done

  • +
  • Railway — push your code, Railway sets PORT

  • +
  • Render — set start command to python api.py

  • +
  • Google Cloud Run — containerize and deploy

  • +
  • Azure Container Apps — same pattern

  • +
  • AWS App Runner — and here too

-

Just deploy your code and set the start command to python api.py.

+

The pattern is always the same: deploy your code, set the start command +to python api.py, and the platform handles the rest.

Uvicorn Directly

-

For more control over the production server, you can bypass api.run() -and use uvicorn directly:

+

For production deployments where you want more control, bypass +api.run() and use uvicorn directly:

$ uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4
 
-

This gives you access to all of uvicorn’s options: worker count, SSL -certificates, access logging, and more. See the -uvicorn documentation for details.

+

The --workers flag spawns multiple processes, each handling requests +independently. A good starting point is 2-4 workers per CPU core.

+

Uvicorn supports many options — SSL certificates, access logging, graceful +shutdown timeouts, and more. See the +uvicorn documentation for details.

Reverse Proxy

-

In production, you may want to place Responder behind a reverse proxy like -nginx or Caddy for SSL termination, load balancing, or serving static assets.

+

For high-traffic production deployments, you may want a reverse proxy like +nginx or Caddy in +front of your application for:

+
    +
  • SSL/TLS termination — let the proxy handle HTTPS certificates

  • +
  • Load balancing — distribute traffic across multiple app instances

  • +
  • Static asset serving — offload static files to the proxy

  • +
  • Rate limiting — at the infrastructure level

  • +

Responder’s TrustedHostMiddleware and HTTPSRedirectMiddleware work -correctly behind proxies that set standard forwarding headers.

+correctly behind proxies that set standard forwarding headers +(X-Forwarded-For, X-Forwarded-Proto).

+

That said, uvicorn is production-ready on its own. Many applications run +uvicorn directly without a reverse proxy and do just fine.

diff --git a/index.html b/index.html index a031b3a..9e76c92 100644 --- a/index.html +++ b/index.html @@ -138,9 +138,12 @@ with — you’re in the right place.

  • Serving Files
  • Custom Error Handling
  • Before-Request Hooks
  • +
  • After-Request Hooks
  • WebSocket Support
  • +
  • Server-Sent Events (SSE)
  • GraphQL
  • OpenAPI Documentation
  • +
  • Route Groups
  • Mounting Other Apps
  • Cookies
  • Cookie-Based Sessions
  • @@ -148,10 +151,6 @@ with — you’re in the right place.

  • CORS
  • HSTS
  • Trusted Hosts
  • -
  • Server-Sent Events (SSE)
  • -
  • Streaming Files
  • -
  • After-Request Hooks
  • -
  • Route Groups
  • Request ID
  • Rate Limiting
  • MessagePack
  • diff --git a/quickstart.html b/quickstart.html index 3488ab0..5c8e60f 100644 --- a/quickstart.html +++ b/quickstart.html @@ -43,146 +43,204 @@

    Quick Start

    This guide will walk you through the basics of building a web service with -Responder. By the end, you’ll know how to declare routes, handle requests, -send responses, render templates, and process background tasks.

    +Responder. By the end, you’ll understand how HTTP requests and responses +work, how to define routes, read data from clients, send data back, render +HTML templates, and process work in the background.

    Create a Web Service

    -

    The first thing you need to do is declare a web service. This is the central -object that holds all your routes, middleware, and configuration:

    +

    Every web application starts with a single object — the application +instance. In Responder, this is the API class. It holds your routes, +middleware, templates, and configuration. Think of it as the central +nervous system of your web service:

    import responder
     
     api = responder.API()
     
    +

    That’s it. One import, one line. You now have a fully functional ASGI +application with gzip compression, static file serving, session support, +and a production-ready server — all wired up and ready to go.

    Hello World

    -

    Next, add a route. Here, we’ll make the root URL say “hello, world!”:

    +

    A web service isn’t very useful until it can respond to requests. In HTTP, +a route maps a URL path to a function that handles it. When a client +(like a browser or curl) sends a request to that path, your function +runs and produces a response.

    +

    Here’s the simplest possible route:

    @api.route("/")
     def hello_world(req, resp):
         resp.text = "hello, world!"
     
    -

    Every view receives a req (request) and resp (response) object. You -don’t need to return anything — just mutate the response directly.

    +

    Two things to notice:

    +
      +
    1. Every view function receives two arguments: req (the incoming +request) and resp (the outgoing response).

    2. +
    3. You don’t return anything. Instead, you mutate the response object +directly. This is a deliberate design choice — it keeps the API +consistent whether you’re setting text, JSON, headers, cookies, or +status codes.

    4. +

    Run the Server

    -

    Start your web service with api.run():

    +

    Start your web service with a single call:

    api.run()
     
    -

    This spins up a production-grade uvicorn server on port 5042, ready for -incoming HTTP requests.

    +

    This spins up a production-grade uvicorn +server on port 5042, ready for incoming HTTP requests. Open +http://localhost:5042 in your browser and you’ll see your hello world +response.

    You can customize the port with api.run(port=8000). The PORT environment variable is also honored automatically — when set, Responder -binds to 0.0.0.0 on that port, which is what cloud platforms like -Fly.io, Railway, and Google Cloud Run expect.

    +binds to 0.0.0.0 on that port, which is what cloud platforms expect.

    Note

    Both sync and async views are supported. The async keyword is always -optional — use it when you need to await something.

    +optional — use it when you need to await something, like reading a +request body or querying a database.

    Route Parameters

    -

    If you want dynamic URLs, use Python’s familiar f-string syntax to declare -variables in your routes:

    +

    Static URLs like /about are useful, but most applications need dynamic +routes — URLs that contain variable data, like a user ID or a product slug.

    +

    In Responder, you declare route parameters using Python’s f-string syntax:

    @api.route("/hello/{who}")
     def hello_to(req, resp, *, who):
         resp.text = f"hello, {who}!"
     
    -

    A GET request to /hello/world will respond with hello, world!.

    -

    Route parameters are passed as keyword-only arguments (after the *).

    +

    A GET request to /hello/world will respond with hello, world!. +A request to /hello/guido will respond with hello, guido!.

    +

    Route parameters are passed as keyword-only arguments (after the * +in the function signature). This is a Python feature that makes the +interface explicit — you always know which arguments come from the URL.

    Type Convertors

    -

    You can constrain route parameters to specific types. The parameter will be -automatically converted before it reaches your view:

    +

    By default, route parameters are strings. But often you want them as +integers, UUIDs, or other types. Responder can convert them automatically +using type annotations in the route pattern:

    @api.route("/add/{a:int}/{b:int}")
     async def add(req, resp, *, a, b):
         resp.text = f"{a} + {b} = {a + b}"
     
    +

    Here, a and b will arrive as Python int objects, not strings. +If someone requests /add/3/hello, they’ll get a 404 — the route won’t +match because hello isn’t a valid integer.

    Supported types:

      -
    • str — matches any string without slashes (default)

    • -
    • int — matches digits, converts to int

    • -
    • float — matches decimal numbers, converts to float

    • +
    • str — matches any string without slashes (this is the default)

    • +
    • int — matches digits and converts to int

    • +
    • float — matches decimal numbers and converts to float

    • uuid — matches UUID strings like 550e8400-e29b-41d4-a716-446655440000

    • -
    • path — matches any string including slashes, useful for file paths

    • +
    • path — matches any string including slashes, useful for file paths +like /files/{filepath:path}

    Sending Responses

    -

    Responder gives you several ways to send data back to the client. Just set -the appropriate property on the response object.

    -

    Text and HTML:

    +

    When an HTTP server receives a request, it must send back a response. Every +HTTP response has three parts: a status code (like 200 OK or 404 Not +Found), headers (metadata like Content-Type), and a body (the actual +data).

    +

    Responder lets you set all three by mutating the response object.

    +

    Text and HTML — the simplest response types. resp.text sets the +Content-Type to text/plain, while resp.html sets it to +text/html:

    resp.text = "plain text response"
     resp.html = "<h1>HTML response</h1>"
     
    -

    JSON — the most common pattern for APIs. Set resp.media to any -JSON-serializable Python object:

    +

    JSON — the lingua franca of web APIs. Set resp.media to any +JSON-serializable Python object — a dict, a list, whatever — and Responder +will serialize it to JSON and set the right headers:

    @api.route("/hello/{who}/json")
     def hello_json(req, resp, *, who):
         resp.media = {"hello": who}
     

    If the client sends an Accept: application/x-yaml header, the same data -will be returned as YAML instead. Content negotiation is automatic.

    -

    Files — serve a file from disk with automatic content-type detection:

    +will be returned as YAML instead. This is called content negotiation — +the server and client agree on a format. It happens automatically.

    +

    Files — serve a file from disk. Responder uses Python’s mimetypes +module to figure out the Content-Type from the file extension:

    resp.file("reports/annual.pdf")
     
    -

    Raw bytes:

    +

    Raw bytes — for binary data like images or protocol buffers:

    resp.content = b"\x89PNG\r\n..."
     
    -

    Status codes and headers:

    +

    Status codes — HTTP status codes tell the client what happened. 200 +means success, 201 means something was created, 404 means not found, +500 means the server broke. Set it directly:

    resp.status_code = 201
    -resp.headers["X-Custom"] = "value"
     
    -

    Redirects:

    +

    Headers — HTTP headers carry metadata. Common ones include +Content-Type, Cache-Control, Authorization, and custom +application headers:

    +
    resp.headers["X-Custom"] = "value"
    +
    +
    +

    Redirects — tell the client to go somewhere else:

    api.redirect(resp, location="/new-url")
     
    +

    This sends a 301 Moved Permanently response by default. The client’s +browser will automatically follow the redirect.

    Reading Requests

    -

    The request object gives you access to everything the client sent.

    -

    Method and URL:

    +

    The other half of HTTP is the request — the data the client sends to your +server. This includes the HTTP method (GET, POST, PUT, DELETE), the URL, +headers, query parameters, cookies, and optionally a body.

    +

    Responder wraps all of this in the req object.

    +

    Method and URL — every HTTP request has a method (what the client wants +to do) and a URL (what resource it’s about):

    req.method      # "get", "post", etc. (lowercase)
     req.full_url    # "http://example.com/path?q=1"
     req.url         # parsed URL object
     
    -

    Headers — case-insensitive, just like you’d expect:

    +

    Headers — HTTP headers carry metadata from the client, like what +content types it accepts, authentication tokens, and more. Responder’s +headers dict is case-insensitive, because the HTTP spec says header names +are case-insensitive:

    req.headers["Content-Type"]
     req.headers["content-type"]  # same thing
     
    -

    Query parameters:

    +

    Query parameters — the part of the URL after the ?. These are +commonly used for search, filtering, and pagination:

    # GET /search?q=python&page=2
     req.params["q"]     # "python"
     req.params["page"]  # "2"
     
    -

    Path parameters — also available on the request object:

    +

    Note that query parameters are always strings. If you need an integer, +you’ll need to convert it yourself: int(req.params["page"]).

    +

    Path parameters — the dynamic parts of the URL that matched your route +pattern. These are also available on the request object, which is useful +in before-request hooks where they aren’t passed as function arguments:

    req.path_params["user_id"]  # same as the keyword argument
     
    -

    Request body — for POST/PUT/PATCH requests, you need to await the -body content:

    -
    # JSON body
    +

    Request body — for POST, PUT, and PATCH requests, the client sends +data in the body. Since reading the body is an I/O operation, you need to +await it:

    +
    # JSON body (the most common format for APIs)
     data = await req.media()
     
    -# Form data
    +# Form data (from HTML forms)
     data = await req.media("form")
     
    -# File uploads
    +# File uploads (multipart)
     files = await req.media("files")
     
     # Raw bytes
    @@ -193,40 +251,56 @@ body content:

    Other useful properties:

    -
    req.is_json     # True if content type is JSON
    -req.cookies     # dict of cookies
    -req.session     # session data (dict)
    -req.client      # (host, port) tuple
    -req.is_secure   # True if HTTPS
    +
    req.is_json     # True if the content type is JSON
    +req.cookies     # dict of cookies sent by the client
    +req.session     # session data (a signed, server-side dict)
    +req.client      # (host, port) tuple — the client's IP address
    +req.is_secure   # True if the request came over HTTPS
     

    Rendering Templates

    -

    Responder includes built-in Jinja2 -support. Templates are loaded from the templates/ directory by default.

    -

    The simplest way is to use api.template():

    +

    While APIs typically return JSON, many web applications need to render +HTML pages. Responder includes built-in support for +Jinja2, one of the most popular +templating engines in the Python ecosystem.

    +

    Templates let you write HTML with placeholders that get filled in with +dynamic data. This keeps your presentation logic (HTML) separate from +your application logic (Python) — a pattern called +separation of concerns.

    +

    The simplest way to render a template is api.template(). Templates +are loaded from the templates/ directory by default:

    @api.route("/hello/{name}/html")
     def hello_html(req, resp, *, name):
         resp.html = api.template("hello.html", name=name)
     
    -

    You can also use the Templates class directly for more control:

    +

    The template file templates/hello.html might look like:

    +
    <h1>Hello, {{ name }}!</h1>
    +
    +
    +

    The {{ name }} part is a Jinja2 expression — it gets replaced with +the value you passed in.

    +

    You can also use the Templates class directly for more control over +the template directory and configuration:

    from responder.templates import Templates
     
    -templates = Templates(directory="templates")
    +templates = Templates(directory="my_templates")
     
     @api.route("/page")
     def page(req, resp):
         resp.html = templates.render("page.html", title="Hello")
     
    -

    Async rendering is supported too:

    +

    For applications that need non-blocking template rendering (rare, but +useful under extreme load), async rendering is supported:

    templates = Templates(directory="templates", enable_async=True)
     resp.html = await templates.render_async("page.html", title="Hello")
     
    -

    You can render template strings without a file:

    +

    And for quick one-off templates, you can render a string directly without +a file:

    resp.html = api.template_string("Hello, {{ name }}!", name="world")
     
    @@ -234,7 +308,12 @@ support. Templates are loaded from the

    Background Tasks

    Sometimes you want to accept a request, respond immediately, and do the -actual processing later. Responder makes this easy with background tasks:

    +actual processing later. This is a common pattern for operations that take +a long time — sending emails, processing images, updating caches, or +calling slow external APIs.

    +

    Responder makes this easy with background tasks. Decorate any function +with @api.background.task and it will run in a thread pool, separate +from the request/response cycle:

    @api.route("/incoming")
     async def receive_incoming(req, resp):
         data = await req.media()
    @@ -247,12 +326,21 @@ actual processing later. Responder makes this easy with background tasks:

    process_data(data) - # Respond immediately — processing continues in the background + # This response is sent immediately, while process_data + # continues running in the background. resp.media = {"status": "accepted"}
    -

    The @api.background.task decorator wraps any function to run in a thread -pool. The client gets an immediate response while the work continues.

    +

    The client gets an instant response — the heavy lifting happens after. +This is the same pattern used by task queues like Celery, but much simpler +for lightweight use cases where you don’t need a full message broker.

    +
    +

    Note

    +

    Background tasks run in threads, not processes. They share memory with +your application, which makes them fast to start but means CPU-intensive +work will block the event loop. For heavy computation, consider a proper +task queue.

    +
    diff --git a/searchindex.js b/searchindex.js index d529729..b92d5d5 100644 --- a/searchindex.js +++ b/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles":{"API Documentation":[[0,null]],"Added":[[2,"added"],[2,"id1"],[2,"id7"],[2,"id8"],[2,"id11"],[2,"id14"],[2,"id19"],[2,"id26"],[2,"id32"],[2,"id33"],[2,"id34"],[2,"id35"],[2,"id36"],[2,"id39"],[2,"id40"],[2,"id42"],[2,"id45"],[2,"id48"],[2,"id49"],[2,"id50"],[2,"id54"]],"After-Request Hooks":[[9,"after-request-hooks"]],"Background Tasks":[[6,"background-tasks"]],"Backlog":[[1,null]],"Before-Request Hooks":[[9,"before-request-hooks"]],"Build JavaScript Application":[[3,"build-javascript-application"]],"CORS":[[9,"cors"]],"Changed":[[2,"changed"],[2,"id6"],[2,"id9"],[2,"id12"],[2,"id15"],[2,"id18"],[2,"id23"],[2,"id24"],[2,"id25"],[2,"id27"],[2,"id28"],[2,"id30"],[2,"id31"],[2,"id37"],[2,"id41"],[2,"id46"],[2,"id47"],[2,"id53"]],"Changelog":[[2,null]],"Class-Based Views":[[9,"class-based-views"]],"Cloud Platforms":[[4,"cloud-platforms"]],"Cookie-Based Sessions":[[9,"cookie-based-sessions"]],"Cookies":[[9,"cookies"]],"Create a Web Service":[[6,"create-a-web-service"]],"Custom Error Handling":[[9,"custom-error-handling"]],"Deployment":[[4,null]],"Deprecated":[[2,"deprecated"]],"Development Sandbox":[[7,null]],"Docker":[[4,"docker"]],"Feature Tour":[[9,null]],"Fixed":[[2,"fixed"],[2,"id2"],[2,"id3"],[2,"id4"],[2,"id5"],[2,"id10"],[2,"id13"],[2,"id16"],[2,"id17"],[2,"id20"],[2,"id21"],[2,"id22"],[2,"id29"],[2,"id38"],[2,"id43"],[2,"id44"],[2,"id51"],[2,"id52"]],"Future Ideas":[[1,"future-ideas"]],"Getting Started":[[8,"getting-started"]],"GraphQL":[[9,"graphql"]],"HSTS":[[9,"hsts"]],"Hello World":[[6,"hello-world"]],"Installation":[[5,"installation"]],"Launch Local File":[[3,"launch-local-file"]],"Launch Module Entrypoint":[[3,"launch-module-entrypoint"]],"Launch Remote File":[[3,"launch-remote-file"]],"Launch with Non-Standard Instance Name":[[3,"launch-with-non-standard-instance-name"]],"Lifespan Events":[[9,"lifespan-events"]],"MessagePack":[[9,"messagepack"]],"Method Filtering":[[9,"method-filtering"]],"Mounting Other Apps":[[9,"mounting-other-apps"]],"OpenAPI Documentation":[[9,"openapi-documentation"]],"Operations":[[7,"operations"]],"Project":[[5,null]],"Quick Start":[[6,null]],"Rate Limiting":[[9,"rate-limiting"]],"Reading Requests":[[6,"reading-requests"]],"Removed":[[2,"removed"]],"Rendering Templates":[[6,"rendering-templates"]],"Request ID":[[9,"request-id"]],"Requests & Responses":[[0,"requests-responses"]],"Responder":[[5,null]],"Responder CLI":[[3,null]],"Reverse Proxy":[[4,"reverse-proxy"]],"Route Groups":[[9,"route-groups"]],"Route Parameters":[[6,"route-parameters"]],"Run the Server":[[6,"run-the-server"]],"Running Locally":[[4,"running-locally"]],"Sending Responses":[[6,"sending-responses"]],"Server-Sent Events (SSE)":[[9,"server-sent-events-sse"]],"Serving Files":[[9,"serving-files"]],"Setup":[[7,"setup"]],"Static Files":[[9,"static-files"]],"Streaming Files":[[9,"streaming-files"]],"Testing":[[8,null]],"Testing Before and After Hooks":[[8,"testing-before-and-after-hooks"]],"Testing Error Handling":[[8,"testing-error-handling"]],"Testing File Uploads":[[8,"testing-file-uploads"]],"Testing Headers and Cookies":[[8,"testing-headers-and-cookies"]],"Testing JSON APIs":[[8,"testing-json-apis"]],"Testing Lifespan Events":[[8,"testing-lifespan-events"]],"Testing Request Validation":[[8,"testing-request-validation"]],"Testing WebSockets":[[8,"testing-websockets"]],"The Idea":[[5,"the-idea"]],"Tips":[[8,"tips"]],"Trusted Hosts":[[9,"trusted-hosts"]],"Type Convertors":[[6,"type-convertors"]],"Unreleased":[[2,"unreleased"]],"User Guide":[[5,null]],"Using Fixtures":[[8,"using-fixtures"]],"Utility Functions":[[0,"utility-functions"]],"Uvicorn Directly":[[4,"uvicorn-directly"]],"Web Service (API) Class":[[0,"module-responder"]],"WebSocket Support":[[9,"websocket-support"]],"What You Get":[[5,"what-you-get"]],"v0.0.1 - 2018-10-12":[[2,"v0-0-1-2018-10-12"]],"v0.0.10 - 2018-10-17":[[2,"v0-0-10-2018-10-17"]],"v0.0.2 - 2018-10-13":[[2,"v0-0-2-2018-10-13"]],"v0.0.3 - 2018-10-13":[[2,"v0-0-3-2018-10-13"]],"v0.0.4 - 2018-10-15":[[2,"v0-0-4-2018-10-15"]],"v0.0.5 - 2018-10-15":[[2,"v0-0-5-2018-10-15"]],"v0.0.6 - 2018-10-16":[[2,"v0-0-6-2018-10-16"]],"v0.0.7 - 2018-10-16":[[2,"v0-0-7-2018-10-16"]],"v0.0.8 - 2018-10-17":[[2,"v0-0-8-2018-10-17"]],"v0.0.9 - 2018-10-17":[[2,"v0-0-9-2018-10-17"]],"v0.1.0 - 2018-10-17":[[2,"v0-1-0-2018-10-17"]],"v0.1.1 - 2018-10-17":[[2,"v0-1-1-2018-10-17"]],"v0.1.2 - 2018-10-18":[[2,"v0-1-2-2018-10-18"]],"v0.1.3 - 2018-10-18":[[2,"v0-1-3-2018-10-18"]],"v0.1.4 - 2018-10-19":[[2,"v0-1-4-2018-10-19"]],"v0.1.5 - 2018-10-20":[[2,"v0-1-5-2018-10-20"]],"v0.1.6 - 2018-10-20":[[2,"v0-1-6-2018-10-20"]],"v0.2.0 - 2018-10-22":[[2,"v0-2-0-2018-10-22"]],"v0.2.1 - 2018-10-23":[[2,"v0-2-1-2018-10-23"]],"v0.2.2 - 2018-10-23":[[2,"v0-2-2-2018-10-23"]],"v0.2.3 - 2018-10-24":[[2,"v0-2-3-2018-10-24"]],"v0.3.0 - 2018-10-24":[[2,"v0-3-0-2018-10-24"]],"v0.3.1 - 2018-10-24":[[2,"v0-3-1-2018-10-24"]],"v0.3.2 - 2018-10-25":[[2,"v0-3-2-2018-10-25"]],"v0.3.3 - 2018-10-25":[[2,"v0-3-3-2018-10-25"]],"v1.0.0 - 2018-10-26":[[2,"v1-0-0-2018-10-26"]],"v1.0.1 - 2018-10-26":[[2,"v1-0-1-2018-10-26"]],"v1.0.2 - 2018-10-27":[[2,"v1-0-2-2018-10-27"]],"v1.0.3 - 2018-10-27":[[2,"v1-0-3-2018-10-27"]],"v1.0.4 - 2018-10-27":[[2,"v1-0-4-2018-10-27"]],"v1.0.5- 2018-10-27":[[2,"v1-0-5-2018-10-27"]],"v1.1.0 - 2018-10-27":[[2,"v1-1-0-2018-10-27"]],"v1.1.1 - 2018-10-29":[[2,"v1-1-1-2018-10-29"]],"v1.1.2 - 2018-11-11":[[2,"v1-1-2-2018-11-11"]],"v1.1.3 - 2019-01-12":[[2,"v1-1-3-2019-01-12"]],"v1.2.0 - 2018-12-29":[[2,"v1-2-0-2018-12-29"]],"v1.3.0 - 2019-02-22":[[2,"v1-3-0-2019-02-22"]],"v1.3.1 - 2019-04-28":[[2,"v1-3-1-2019-04-28"]],"v1.3.2 - 2019-08-15":[[2,"v1-3-2-2019-08-15"]],"v2.0.0 - 2019-09-19":[[2,"v2-0-0-2019-09-19"]],"v2.0.1 - 2019-09-20":[[2,"v2-0-1-2019-09-20"]],"v2.0.2 - 2019-09-20":[[2,"v2-0-2-2019-09-20"]],"v2.0.3 - 2019-09-20":[[2,"v2-0-3-2019-09-20"]],"v2.0.4 - 2019-11-19":[[2,"v2-0-4-2019-11-19"]],"v2.0.5 - 2019-12-15":[[2,"v2-0-5-2019-12-15"]],"v3.0.0 - 2026-03-22":[[2,"v3-0-0-2026-03-22"]]},"docnames":["api","backlog","changes","cli","deployment","index","quickstart","sandbox","testing","tour"],"envversion":{"sphinx":65,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.viewcode":1},"filenames":["api.rst","backlog.md","changes.md","cli.rst","deployment.rst","index.rst","quickstart.rst","sandbox.md","testing.rst","tour.rst"],"indexentries":{"accepts() (responder.request method)":[[0,"responder.Request.accepts",false]],"add_event_handler() (responder.api method)":[[0,"responder.API.add_event_handler",false]],"add_route() (responder.api method)":[[0,"responder.API.add_route",false]],"after_request() (responder.api method)":[[0,"responder.API.after_request",false]],"api (class in responder)":[[0,"responder.API",false]],"apparent_encoding (responder.request property)":[[0,"responder.Request.apparent_encoding",false]],"client (responder.request property)":[[0,"responder.Request.client",false]],"content (responder.request property)":[[0,"responder.Request.content",false]],"content (responder.response attribute)":[[0,"responder.Response.content",false]],"cookies (responder.request property)":[[0,"responder.Request.cookies",false]],"cookies (responder.response attribute)":[[0,"responder.Response.cookies",false]],"encoding (responder.request property)":[[0,"responder.Request.encoding",false]],"exception_handler() (responder.api method)":[[0,"responder.API.exception_handler",false]],"file() (responder.response method)":[[0,"responder.Response.file",false]],"formats (responder.response attribute)":[[0,"responder.Response.formats",false]],"full_url (responder.request property)":[[0,"responder.Request.full_url",false]],"graphql() (responder.api method)":[[0,"responder.API.graphql",false]],"group() (responder.api method)":[[0,"responder.API.group",false]],"headers (responder.request property)":[[0,"responder.Request.headers",false]],"headers (responder.response attribute)":[[0,"responder.Response.headers",false]],"is_100() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_100",false]],"is_200() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_200",false]],"is_300() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_300",false]],"is_400() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_400",false]],"is_500() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_500",false]],"is_json (responder.request property)":[[0,"responder.Request.is_json",false]],"media (responder.response attribute)":[[0,"responder.Response.media",false]],"media() (responder.request method)":[[0,"responder.Request.media",false]],"method (responder.request property)":[[0,"responder.Request.method",false]],"module":[[0,"module-responder",false]],"mount() (responder.api method)":[[0,"responder.API.mount",false]],"on_event() (responder.api method)":[[0,"responder.API.on_event",false]],"params (responder.request property)":[[0,"responder.Request.params",false]],"path_matches_route() (responder.api method)":[[0,"responder.API.path_matches_route",false]],"path_params (responder.request property)":[[0,"responder.Request.path_params",false]],"redirect() (responder.api method)":[[0,"responder.API.redirect",false]],"request (class in responder)":[[0,"responder.Request",false]],"requests (responder.api property)":[[0,"responder.API.requests",false]],"responder":[[0,"module-responder",false]],"response (class in responder)":[[0,"responder.Response",false]],"route() (responder.api method)":[[0,"responder.API.route",false]],"schema() (responder.api method)":[[0,"responder.API.schema",false]],"serve() (responder.api method)":[[0,"responder.API.serve",false]],"session (responder.request property)":[[0,"responder.Request.session",false]],"session (responder.response attribute)":[[0,"responder.Response.session",false]],"session() (responder.api method)":[[0,"responder.API.session",false]],"sse() (responder.response method)":[[0,"responder.Response.sse",false]],"state (responder.request property)":[[0,"responder.Request.state",false]],"status_code (responder.response attribute)":[[0,"responder.Response.status_code",false]],"stream_file() (responder.response method)":[[0,"responder.Response.stream_file",false]],"template() (responder.api method)":[[0,"responder.API.template",false]],"template_string() (responder.api method)":[[0,"responder.API.template_string",false]],"text (responder.request property)":[[0,"responder.Request.text",false]],"url (responder.request property)":[[0,"responder.Request.url",false]],"url_for() (responder.api method)":[[0,"responder.API.url_for",false]]},"objects":{"":[[0,0,0,"-","responder"]],"responder":[[0,1,1,"","API"],[0,1,1,"","Request"],[0,1,1,"","Response"]],"responder.API":[[0,2,1,"","add_event_handler"],[0,2,1,"","add_route"],[0,2,1,"","after_request"],[0,2,1,"","exception_handler"],[0,2,1,"","graphql"],[0,2,1,"","group"],[0,2,1,"","mount"],[0,2,1,"","on_event"],[0,2,1,"","path_matches_route"],[0,2,1,"","redirect"],[0,3,1,"","requests"],[0,2,1,"","route"],[0,2,1,"","schema"],[0,2,1,"","serve"],[0,2,1,"","session"],[0,2,1,"","template"],[0,2,1,"","template_string"],[0,2,1,"","url_for"]],"responder.API.status_codes":[[0,4,1,"","is_100"],[0,4,1,"","is_200"],[0,4,1,"","is_300"],[0,4,1,"","is_400"],[0,4,1,"","is_500"]],"responder.Request":[[0,2,1,"","accepts"],[0,3,1,"","apparent_encoding"],[0,3,1,"","client"],[0,3,1,"","content"],[0,3,1,"","cookies"],[0,3,1,"","encoding"],[0,3,1,"","full_url"],[0,3,1,"","headers"],[0,3,1,"","is_json"],[0,2,1,"","media"],[0,3,1,"","method"],[0,3,1,"","params"],[0,3,1,"","path_params"],[0,3,1,"","session"],[0,3,1,"","state"],[0,3,1,"","text"],[0,3,1,"","url"]],"responder.Response":[[0,5,1,"","content"],[0,5,1,"","cookies"],[0,2,1,"","file"],[0,5,1,"","formats"],[0,5,1,"","headers"],[0,5,1,"","media"],[0,5,1,"","session"],[0,2,1,"","sse"],[0,5,1,"","status_code"],[0,2,1,"","stream_file"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","property","Python property"],"4":["py","function","Python function"],"5":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:method","3":"py:property","4":"py:function","5":"py:attribute"},"terms":{"":[0,2,3,4,5,6,8,9],"0":[3,4,6,9],"1":[0,3,4,6,9],"10":[0,6,9],"100":9,"127":[3,4],"13":[3,4],"16":3,"2":[6,8,9],"200":[3,8,9],"201":[6,8],"2024":3,"25":3,"26":3,"3":[4,5,8,9],"301":0,"308":2,"3600":9,"4":4,"40":2,"400":[0,8,9],"401":9,"41d4":6,"422":8,"429":9,"446655440000":6,"500":[2,8,9],"5042":[3,4,6],"55":3,"550e8400":6,"60":9,"600":[0,9],"8":[0,3],"80":4,"8000":[4,6],"8192":0,"9":[5,8],"99":8,"A":[0,3,4,5,6,9],"But":8,"By":[3,6,8,9],"For":[3,4,8,9],"If":[0,3,5,6,8,9],"In":[3,4,9],"It":[3,5,8,9],"No":[8,9],"One":[5,8],"That":[5,8],"The":[0,2,3,4,6,8,9],"There":[8,9],"To":3,"Will":0,"With":0,"__main__":[4,5,8],"__name__":[4,5,8,9],"_route_for":2,"a2wsgi":2,"a716":6,"abc123":[8,9],"abf":3,"abil":2,"abl":0,"about":8,"abov":[3,5],"accept":[0,2,3,6,8,9],"access":[3,4,6,9],"acm":3,"acquir":[3,7],"activ":7,"actual":[0,6,9],"ad":[1,9],"adapt":3,"add":[0,1,2,6,9],"add_directori":9,"add_event_handl":0,"add_head":9,"add_request_id":0,"add_rout":[0,9],"add_tim":8,"add_vers":8,"addit":[0,9],"address":[0,3],"adher":2,"after":[0,5,6],"after_request":[0,1,8,9],"ag":9,"against":[0,9],"ai":9,"alias":2,"alic":9,"all":[0,2,3,4,6,7,8,9],"alloc":8,"allow":9,"allow_credenti":[0,9],"allow_head":[0,9],"allow_method":[0,9],"allow_origin":[0,9],"allow_origin_regex":0,"allowed_host":[0,9],"alreadi":[0,8,9],"also":[0,2,3,6,8,9],"altern":0,"alwai":[5,6,9],"an":[0,3,6,9],"ani":[0,5,6,8,9],"annot":[5,9],"annual":[6,9],"anyon":5,"anyth":6,"anywher":4,"api":[2,3,4,5,6,9],"apispec":[2,9],"app":[0,2,3,4,5,8],"appar":0,"apparent_encod":0,"appear":5,"append":3,"applic":[0,2,4,5,6,8,9],"approach":9,"appropri":[6,9],"approx":3,"ar":[2,3,4,5,6,8,9],"arg":0,"argument":[0,2,6],"around":[0,5],"arrow":3,"asgi":[0,2,4,5,9],"ask":5,"assert":8,"asset":[3,4,9],"assum":3,"async":[0,1,2,5,6,8,9],"asynccontextmanag":9,"asynchron":2,"attack":9,"attr":3,"attribut":[0,3],"auth":5,"auth_check":9,"authent":9,"author":[3,7,9],"auto":9,"auto_escap":0,"autobuild":7,"automat":[0,4,5,6,8,9],"avail":[3,6],"avoid":8,"aw":[3,4],"await":[0,6,8,9],"az":3,"azur":[3,4],"b":[6,8],"back":[0,5,6],"background":[2,5],"backlog":5,"bad":[8,9],"balanc":4,"base":[0,2,3,5],"base_url":0,"basemodel":[0,8,9],"basic":[3,6],"batteri":5,"battl":5,"becom":0,"been":3,"befor":[5,6],"before_request":[0,1,2,8,9],"beforehand":3,"behind":4,"being":0,"best":5,"better":5,"bidirect":9,"bin":7,"binari":9,"bind":[0,4,6],"blob":3,"block":8,"bodi":[0,6,8,9],"boilerpl":5,"both":[5,6,9],"box":4,"break":8,"bring":5,"broke":8,"browser":[3,7,9],"bucket":[3,9],"bufix":2,"bug":[2,8],"bugfix":2,"build":[2,4,5,6,7],"built":[2,5,6,8,9],"bump":2,"bypass":4,"byte":[0,5,6],"cach":[8,9],"caddi":4,"call":[8,9],"callabl":0,"can":[0,3,4,5,6,8,9],"capac":3,"case":[0,5,6],"cat":9,"cd":7,"ceas":2,"central":6,"certain":0,"certif":4,"chanc":3,"chang":3,"changelog":5,"chardet":0,"check":[7,8],"check_exist":[0,9],"choos":[3,9],"chunk":0,"chunk_siz":0,"ci":2,"circuit":[1,5,9],"class":[2,5,6,8],"clean":[5,9],"cleanup":9,"cli":[2,5],"click":5,"client":[0,5,6,8,9],"clone":7,"close":[8,9],"close_database_connection_pool":0,"cloud":[3,5,6],"cmd":4,"code":[0,2,4,6,7,8,9],"com":[3,6,7,9],"command":[3,4],"common":[3,6,9],"commun":9,"compat":3,"complement":1,"complex":9,"compon":9,"compress":5,"concept":2,"concern":9,"conclud":3,"condit":8,"configur":[2,6,8],"conflict":2,"confus":8,"connect":[0,8,9],"consid":1,"constrain":6,"contact":0,"contain":[0,4],"content":[0,2,3,5,6,8,9],"content_typ":[0,8,9],"context":[5,8,9],"contextlib":9,"continu":6,"contract":8,"contribut":9,"control":[4,6,9],"convert":[2,6],"convertor":5,"cooki":[0,2,5,6],"copi":[4,9],"cor":[0,2,5,8],"core":2,"coroutin":0,"correctli":4,"correspond":3,"cors_param":[0,9],"count":[2,4],"creat":[0,5,7,8,9],"create_item":[0,9],"create_pet":9,"cross":9,"css":9,"csv":9,"curl":3,"current":3,"custom":[0,5,6,8],"d":[4,6,8],"dask":3,"data":[0,2,6,8,9],"databas":[8,9],"dataset":9,"date":3,"debug":[0,2,9],"decim":6,"declar":[2,6],"decod":[2,9],"decor":[0,6,9],"deep":5,"def":[0,4,5,6,8,9],"default":[0,1,2,3,6,8,9],"default_valu":[0,9],"defin":[3,9],"definit":0,"denpend":2,"depend":[2,8],"deploi":[4,5],"deploy":5,"descript":[0,9],"design":3,"detail":[4,9],"detect":[0,5,6,9],"dev":2,"dict":[0,5,6,9],"dictionari":[0,9],"differ":[3,8,9],"digit":6,"direct":[2,9],"directli":[5,6,8,9],"directori":[0,3,6,9],"disabl":8,"disk":[0,6,9],"dispatch":[2,9],"distribut":3,"django":5,"dna":5,"do":[3,6],"doc":[7,8,9],"docker":5,"dockerfil":4,"docs_rout":[0,9],"docstr":9,"document":[2,4,5,7],"doesn":[0,9],"domain":9,"don":[3,6,8],"down":9,"download":9,"dramat":2,"drop":2,"dropbox":3,"dump":2,"dure":8,"dynam":6,"e":0,"e29b":6,"each":[0,8,9],"easi":6,"echo":3,"ecosystem":5,"edit":[3,7],"effect":8,"effici":9,"either":[0,3],"element":[0,9],"els":9,"enabl":[2,9],"enable_async":6,"enable_hst":[0,9],"encod":[0,3],"end":6,"endpoint":[0,2,8,9],"enter":8,"entir":[0,9],"entri":3,"entrypoint":5,"enumer":3,"env":4,"environ":[0,4,6],"eol":2,"error":[0,5],"escap":0,"etc":[6,8,9],"even":3,"event":[0,2,5],"event_typ":0,"everi":[0,5,6,8,9],"everyth":[6,9],"exactli":8,"exampl":[3,6,9],"exc":[0,8,9],"exceed":9,"except":[0,2,5,8,9],"exception_cl":0,"exception_handl":[0,8,9],"exist":[0,2,3,5],"exit":8,"expect":[3,6],"expir":9,"explicit":9,"explicitli":9,"explor":[1,5,9],"expos":4,"expose_head":0,"express":5,"ext":[2,9],"extens":2,"extra":[2,3],"extra_asset":9,"extract":0,"f":[0,5,6,8,9],"fail":8,"failur":8,"falcon":5,"fall":0,"fals":[0,8,9],"familiar":[5,6],"fast":[5,8],"fastapi":5,"favourit":3,"featur":[0,5],"feed":9,"feel":5,"few":3,"fido":9,"field":[0,8,9],"figur":[5,9],"file":[0,2,5,6],"filenam":[0,8],"filesystem":[2,3],"filter":5,"fire":8,"first":[6,9],"fit":5,"fix":7,"fixtur":5,"flag":9,"flask":[5,9],"flask_app":9,"float":[0,6,8],"fly":[4,6],"forc":9,"form":[0,3,6],"format":[0,2,7,9],"forward":[4,5,9],"found":2,"framework":[5,9],"fresh":8,"from":[0,2,3,4,5,6,8,9],"frontend":9,"fsspec":3,"full":[0,2,3,9],"full_url":[0,6,9],"fun":5,"func":0,"function":[5,6],"fuse":3,"g":[0,3],"gc":3,"gener":[0,5,8,9],"get":[0,6,9],"get_us":[0,9],"git":[3,7],"github":[3,7],"give":[3,4,6,8,9],"given":[0,8],"gmt":3,"goe":9,"good":5,"googl":[3,4,6],"grade":6,"gradual":9,"graphen":[0,2,5,9],"graphiql":[2,5,9],"graphql":[0,2,5],"great":[5,9],"greet":[5,9],"greet_world":5,"greetingresourc":9,"group":[0,5],"guard":[5,9],"guid":6,"gzip":5,"h1":6,"ha":[8,9],"hadoop":3,"hand":[0,3],"handl":[5,6],"handle_value_error":[0,9],"handler":[0,3,5,8,9],"happen":5,"hard":8,"have":[0,2,3,8],"hdf":3,"head":3,"header":[0,4,5,6,9],"heavi":6,"hello":[0,3,4,5,8,9],"hello_html":6,"hello_json":6,"hello_to":6,"hello_world":6,"helloworld":3,"help":0,"here":[6,9],"hold":[5,6],"home":5,"honor":[4,6],"hook":[1,5],"host":[0,2,3,4,5,6],"hostnam":9,"how":6,"hst":5,"html":[0,2,5,6,9],"http":[0,2,3,5,6,7,8,9],"httpie":3,"httponli":9,"httpsredirectmiddlewar":4,"httpx":[2,8],"i":[0,2,3,4,5,6,8,9],"id":[0,5],"imag":9,"immedi":[5,6,9],"immut":2,"implement":[3,8],"import":[0,2,3,4,5,6,8,9],"importlib":3,"improv":2,"includ":[3,5,6,7,8,9],"incom":[0,6],"index":[0,9],"info":[0,2,9],"inform":0,"infrastructur":5,"inherit":[3,9],"initi":[0,8],"inlin":9,"input":8,"insensit":[0,5,6],"instal":[0,3,4,7,9],"instanc":[0,5,8],"instead":[3,6,8,9],"int":[0,6,9],"integ":0,"integr":9,"intent":5,"interact":[2,9],"intern":[8,9],"internet":3,"invalid":8,"invoc":3,"invok":3,"io":[4,6],"is_100":0,"is_200":0,"is_300":0,"is_400":0,"is_500":0,"is_class_bas":2,"is_json":[0,6],"is_secur":6,"isn":9,"issu":2,"item":[0,8,9],"itemin":0,"itemout":0,"its":[0,2,3],"javascript":[5,9],"jinja2":[0,6],"joi":5,"jpeg":9,"jpg":9,"json":[0,3,5,6,9],"just":[4,6,8,9],"keep":[2,8,9],"kei":[0,8,9],"kennethreitz":[3,7],"keyword":[0,6],"kind":3,"know":6,"known":0,"kwarg":0,"larg":[5,9],"larger":8,"later":[6,8],"launch":5,"layout":3,"lazili":0,"lead":8,"learn":5,"length":3,"less":9,"librari":8,"licens":0,"lifespan":[0,2,5],"like":[3,4,5,6,8,9],"limit":[1,5],"line":3,"link":9,"list":[0,3,8,9],"list_item":9,"list_pet":9,"list_us":[0,9],"live":9,"ll":[5,6,9],"load":[0,2,3,4,6,9],"local":5,"locat":[0,3,6],"log":[4,9],"log_level":2,"log_respons":9,"logic":[5,9],"login":9,"look":5,"lower":0,"lowercas":6,"mai":4,"main":3,"make":[6,8,9],"manag":[5,8,9],"mani":9,"manual":0,"map":2,"mark":2,"marshmallow":[0,2,9],"match":[0,6,9],"max_ag":[0,9],"mean":9,"media":[0,2,5,6,8,9],"memori":[0,9],"messag":[0,9],"messagepack":5,"method":[0,5,6,8],"middelwar":0,"middlewar":[1,2,6,9],"migrat":[2,9],"mime":0,"mimetyp":9,"min":9,"minim":[3,4],"minimum":2,"minor":2,"miss":[8,9],"mix":9,"ml":9,"mode":[0,7],"model":[0,8,9],"modern":9,"modul":[2,5,9],"more":[3,4,5,6,9],"most":[4,6,8],"mount":[0,2,5],"move":2,"msgpack":9,"much":9,"multipart":2,"multipl":[2,3,9],"must":[0,9],"mutabl":5,"mutat":[0,5,6],"myapi":4,"n":6,"name":[0,5,6,8,9],"namespac":2,"natur":8,"need":[3,5,6,8,9],"negoti":[0,5,6,8,9],"network":8,"never":9,"new":[0,6,8,9],"next":[3,6],"nginx":4,"non":5,"none":[0,2],"notabl":2,"notasecret":0,"note":9,"now":[2,9],"npm":3,"number":6,"object":[0,2,3,5,6,9],"objecttyp":[0,9],"obviou":8,"oci":3,"oct":3,"often":9,"ok":[3,8],"on_delet":9,"on_ev":[0,8,9],"on_get":[5,9],"on_post":[5,9],"on_put":9,"on_request":[5,9],"on_startup":8,"onc":9,"one":[0,3,5],"onli":6,"open":[2,7],"open_database_connection_pool":0,"openapi":[0,2,5],"openapi_rout":0,"openapi_them":0,"option":[0,2,3,4,5,6],"order":3,"organ":[8,9],"origin":9,"other":[0,2,3,5,6],"otherwis":[0,9],"out":[4,5,9],"over":[0,4,5,9],"overal":2,"overhead":8,"overrid":[0,9],"p":4,"packag":[2,3],"page":[2,6,9],"painless":8,"param":[0,2,5,6],"paramet":[0,5,8,9],"parameter":0,"pars":[0,2,6,8],"pass":[0,5,6,8,9],"passion":5,"password":3,"patch":6,"path":[0,2,3,5,6,8,9],"path_matches_rout":0,"path_param":[0,6],"pattern":[5,6,9],"pdf":[6,8,9],"per":[8,9],"period":9,"person":5,"pet":[0,9],"petin":9,"petout":9,"petschema":[0,9],"photo":9,"pin":2,"pip":[3,4,5,7],"place":[4,5,9],"plain":[3,6,9],"platform":[2,5,6],"pleas":3,"pleasant":5,"plugin":[2,9],"point":3,"polici":9,"pool":[5,6],"port":[0,3,4,6,8],"portion":0,"post":[0,6,8,9],"potenti":2,"power":[5,8],"prefer":9,"prefix":[0,9],"price":[0,8],"primari":0,"print":9,"process":[5,6,8],"process_data":6,"product":[4,5,6,8,9],"profil":9,"program":3,"programmat":9,"progress":9,"project":[2,3,7],"propag":8,"properli":8,"properti":[0,6,8],"protect":9,"protocol":[3,8],"prototyp":[2,5],"provid":0,"proxi":5,"push":5,"put":6,"py":[2,3,4,8],"pydant":[0,8,9],"pyproject":2,"pytest":[5,7,8],"python":[0,2,3,4,5,6,9],"q":6,"queri":[0,5,6,9],"quick":5,"r":[6,8],"race":8,"railwai":[4,6],"rais":[2,8,9],"raise_server_except":8,"random":0,"rang":[0,9],"rapidoc":[0,9],"rate":[1,5],"ratelimit":9,"rather":8,"raw":[3,5,6,9],"re":[0,5,8,9],"reach":6,"react":2,"read":[0,2,5,9],"readi":[5,6],"real":9,"realli":8,"receiv":[0,6,8,9],"receive_byt":9,"receive_incom":6,"receive_json":9,"receive_text":[8,9],"recommend":9,"redirect":[0,2,6,9],"redoc":[0,9],"reduc":2,"ref":[3,9],"refactor":[2,8],"refer":[3,8],"reflect":3,"regist":[0,8,9],"regular":3,"reject":8,"relat":9,"relationship":9,"releas":7,"reliabl":8,"remain":9,"remot":5,"renam":8,"render":[0,1,4,5,9],"render_async":6,"replac":2,"report":[6,8,9],"repres":0,"represent":0,"req":[0,4,5,6,8,9],"request":[2,3,5],"request_id":[0,9],"request_model":[0,8,9],"requestbodi":9,"requir":[2,8,9],"research":5,"resolv":[2,9],"resolve_hello":[0,9],"resourc":9,"resp":[0,2,4,5,6,8,9],"respond":[0,2,4,6,7,8,9],"respons":[3,5,8,9],"response_model":[0,9],"rest":5,"restrict":9,"result":9,"resum":2,"resume_incomplet":2,"retri":[0,9],"return":[0,2,5,6,8,9],"revers":5,"rfc3986":2,"right":[5,9],"root":6,"rout":[0,2,4,5,8],"router":2,"ruff":7,"run":[0,2,3,5,7,8,9],"runner":4,"runtim":9,"s3":3,"safe":2,"sai":6,"same":[5,6,9],"sandbox":5,"sat":3,"scale":5,"schema":[0,2,5,9],"scope":[0,9],"scratch":5,"search":[0,2,6],"second":3,"secret":9,"secret_kei":[0,9],"section":[3,9],"secur":9,"see":[3,4,8],"select":[0,2,3],"self":[0,9],"semant":2,"send":[0,2,5,8,9],"send_byt":9,"send_json":9,"send_text":[8,9],"sent":[0,5,6],"separ":8,"sequenti":2,"serial":9,"serializ":6,"serv":[0,2,4,5,6,8],"server":[0,2,3,4,5,8],"servestat":2,"servic":[3,4,5,8],"session":[0,2,5,6,8],"session_id":9,"set":[0,2,4,5,6,7,8,9],"set_cooki":[2,9],"set_text":0,"setup":[2,8],"sever":6,"sftp":3,"share":[0,5,8,9],"short":[1,5,9],"should":5,"shouldn":0,"show":2,"shut":9,"shutdown":[0,5,8,9],"shutdwown":2,"side":9,"sign":[5,9],"simpl":[8,9],"simplecooki":0,"simpler":[5,9],"simplest":[4,6],"simul":6,"sinc":8,"singl":[2,3,5,9],"size":0,"skip":9,"slash":6,"sleep":[6,8],"slim":4,"small":[5,8,9],"smb":3,"so":[3,8,9],"someth":[5,6,8],"sometim":6,"sourc":[0,7],"spec":9,"specif":[0,3,6,8,9],"specifi":3,"sphinx":7,"spin":6,"sse":[0,5],"ssh":3,"ssl":4,"stabil":2,"standalon":9,"standard":[4,5],"starlett":[0,2,5,8],"start":[4,5,9],"startup":[0,2,5,8,9],"state":[0,2,8,9],"statement":5,"static":[0,2,4,5],"static_app":9,"static_dir":[0,9],"static_rout":[0,2,9],"statu":[0,2,6,8],"status_cod":[0,5,6,8,9],"stdlib":2,"stop":9,"storag":3,"store":[0,3,9],"str":[0,6,8,9],"straightforward":9,"stranger":[0,9],"stream":[0,2,5],"stream_fil":[0,9],"strict":9,"string":[0,5,6,9],"structur":9,"style":9,"subcommand":3,"subrout":[5,9],"subtl":2,"suit":8,"suppli":2,"support":[0,1,2,3,5,6],"surpris":3,"suspect":5,"swagger":[5,9],"swagger_ui":[0,9],"switch":2,"symbol":3,"sync":[2,5,6],"syntax":[5,6],"synthet":3,"system":3,"t":[0,3,4,6,8,9],"take":5,"tamper":9,"target":[2,3],"task":[2,5],"tear":9,"templat":[0,1,2,5],"template_str":[0,6],"templates_dir":0,"termin":4,"terms_of_servic":0,"test":[0,2,5,7,9],"test_500":[2,8],"test_api":8,"test_create_item":8,"test_custom_error":8,"test_head":8,"test_hello":8,"test_hook":8,"test_json":8,"test_upload":8,"test_valid":8,"test_websocket":8,"test_with_lifespan":8,"testclient":[0,2,8],"text":[0,3,4,5,6,8,9],"than":[3,8],"thei":9,"them":[4,5,8,9],"theme":[0,9],"thi":[0,2,3,4,6,8,9],"thing":[6,8],"those":3,"thread":[5,6],"threadpoolexecutor":2,"three":9,"through":[6,9],"time":[0,3,5,6,8,9],"time_start":0,"tip":5,"titl":[0,6,9],"togeth":[5,9],"token":9,"toml":2,"too":[6,8,9],"tool":7,"toolbelt":2,"tour":5,"trace":9,"traceback":2,"tradit":9,"traffic":9,"transfer":3,"transport":9,"treat":[0,9],"trigger":8,"true":[0,6,8,9],"trust":5,"trustedhostmiddlewar":4,"tupl":[0,6,8],"type":[0,2,3,5,9],"typic":3,"typo":2,"ui":[2,5,9],"unauthor":9,"unhandl":9,"unicod":0,"uniqu":9,"unknown":0,"unmaintain":2,"unmatch":9,"unpin":2,"up":[0,2,6,7,9],"updat":[2,9],"upgrad":[2,7],"upload":[2,5,6],"url":[0,2,3,5,6,8,9],"url_for":[0,8],"urllib":2,"us":[0,2,3,4,5,6,9],"usag":[0,2],"user":[0,3,9],"user_id":[6,9],"usernam":9,"usual":[5,8],"utf":[0,3],"util":5,"uuid":[0,6,9],"uuid4":0,"uv":[3,5,7],"uvicorn":[0,2,3,5,6],"v1":[0,9],"valid":[3,5,9],"valu":[0,5,6,8,9],"valueerror":[0,8,9],"variabl":[0,4,6],"ve":[5,8],"venv":7,"veri":0,"verifi":8,"version":[0,2,3,8,9],"via":[2,9],"view":[0,2,5,6,8],"virtualenv":7,"visit":9,"w":[8,9],"wa":[0,2],"wai":[4,6,9],"walk":[6,9],"want":[0,4,5,6,8,9],"watch":7,"we":6,"web":[3,4,5],"webdav":3,"websocket":[0,1,2,5],"websocket_connect":8,"well":5,"went":5,"wget":3,"what":[6,8],"when":[2,3,4,5,6,8,9],"where":[3,9],"whether":0,"which":[2,3,4,6,8,9],"whichev":5,"while":[6,9],"whitenois":2,"who":[5,6],"widget":8,"wildcard":9,"window":2,"within":[2,3],"without":[0,6,8,9],"won":8,"work":[3,4,5,6,9],"workdir":4,"worker":4,"workgroup":3,"world":[0,3,4,5,8,9],"worri":8,"would":[3,8],"wrap":[6,8,9],"write":9,"ws_auth":9,"wsgi":[0,2,5,9],"x":[0,6,8,9],"x89png":6,"xml":0,"yaml":[0,2,5,6,8,9],"yield":[0,9],"yml":[0,9],"you":[0,3,4,6,8,9],"your":[3,4,6,8,9]},"titles":["API Documentation","Backlog","Changelog","Responder CLI","Deployment","Responder","Quick Start","Development Sandbox","Testing","Feature Tour"],"titleterms":{"0":2,"01":2,"02":2,"03":2,"04":2,"08":2,"09":2,"1":2,"10":2,"11":2,"12":2,"13":2,"15":2,"16":2,"17":2,"18":2,"19":2,"2":2,"20":2,"2018":2,"2019":2,"2026":2,"22":2,"23":2,"24":2,"25":2,"26":2,"27":2,"28":2,"29":2,"3":2,"4":2,"5":2,"6":2,"7":2,"8":2,"9":2,"The":5,"ad":2,"after":[8,9],"api":[0,8],"app":9,"applic":3,"background":6,"backlog":1,"base":9,"befor":[8,9],"build":3,"chang":2,"changelog":2,"class":[0,9],"cli":3,"cloud":4,"convertor":6,"cooki":[8,9],"cor":9,"creat":6,"custom":9,"deploy":4,"deprec":2,"develop":7,"directli":4,"docker":4,"document":[0,9],"entrypoint":3,"error":[8,9],"event":[8,9],"featur":9,"file":[3,8,9],"filter":9,"fix":2,"fixtur":8,"function":0,"futur":1,"get":[5,8],"graphql":9,"group":9,"guid":5,"handl":[8,9],"header":8,"hello":6,"hook":[8,9],"host":9,"hst":9,"id":9,"idea":[1,5],"instal":5,"instanc":3,"javascript":3,"json":8,"launch":3,"lifespan":[8,9],"limit":9,"local":[3,4],"messagepack":9,"method":9,"modul":3,"mount":9,"name":3,"non":3,"openapi":9,"oper":7,"other":9,"paramet":6,"platform":4,"project":5,"proxi":4,"quick":6,"rate":9,"read":6,"remot":3,"remov":2,"render":6,"request":[0,6,8,9],"respond":[3,5],"respons":[0,6],"revers":4,"rout":[6,9],"run":[4,6],"sandbox":7,"send":6,"sent":9,"serv":9,"server":[6,9],"servic":[0,6],"session":9,"setup":7,"sse":9,"standard":3,"start":[6,8],"static":9,"stream":9,"support":9,"task":6,"templat":6,"test":8,"tip":8,"tour":9,"trust":9,"type":6,"unreleas":2,"upload":8,"us":8,"user":5,"util":0,"uvicorn":4,"v0":2,"v1":2,"v2":2,"v3":2,"valid":8,"view":9,"web":[0,6],"websocket":[8,9],"what":5,"world":6,"you":5}}) \ No newline at end of file +Search.setIndex({"alltitles":{"API Documentation":[[0,null]],"Added":[[2,"added"],[2,"id1"],[2,"id7"],[2,"id8"],[2,"id11"],[2,"id14"],[2,"id19"],[2,"id26"],[2,"id32"],[2,"id33"],[2,"id34"],[2,"id35"],[2,"id36"],[2,"id39"],[2,"id40"],[2,"id42"],[2,"id45"],[2,"id48"],[2,"id49"],[2,"id50"],[2,"id54"]],"After-Request Hooks":[[9,"after-request-hooks"]],"Background Tasks":[[6,"background-tasks"]],"Backlog":[[1,null]],"Before-Request Hooks":[[9,"before-request-hooks"]],"Build JavaScript Application":[[3,"build-javascript-application"]],"CORS":[[9,"cors"]],"Changed":[[2,"changed"],[2,"id6"],[2,"id9"],[2,"id12"],[2,"id15"],[2,"id18"],[2,"id23"],[2,"id24"],[2,"id25"],[2,"id27"],[2,"id28"],[2,"id30"],[2,"id31"],[2,"id37"],[2,"id41"],[2,"id46"],[2,"id47"],[2,"id53"]],"Changelog":[[2,null]],"Class-Based Views":[[9,"class-based-views"]],"Cloud Platforms":[[4,"cloud-platforms"]],"Cookie-Based Sessions":[[9,"cookie-based-sessions"]],"Cookies":[[9,"cookies"]],"Create a Web Service":[[6,"create-a-web-service"]],"Custom Error Handling":[[9,"custom-error-handling"]],"Deployment":[[4,null]],"Deprecated":[[2,"deprecated"]],"Development Sandbox":[[7,null]],"Docker":[[4,"docker"]],"Feature Tour":[[9,null]],"Fixed":[[2,"fixed"],[2,"id2"],[2,"id3"],[2,"id4"],[2,"id5"],[2,"id10"],[2,"id13"],[2,"id16"],[2,"id17"],[2,"id20"],[2,"id21"],[2,"id22"],[2,"id29"],[2,"id38"],[2,"id43"],[2,"id44"],[2,"id51"],[2,"id52"]],"Future Ideas":[[1,"future-ideas"]],"Getting Started":[[8,"getting-started"]],"GraphQL":[[9,"graphql"]],"HSTS":[[9,"hsts"]],"Hello World":[[6,"hello-world"]],"Installation":[[5,"installation"]],"Launch Local File":[[3,"launch-local-file"]],"Launch Module Entrypoint":[[3,"launch-module-entrypoint"]],"Launch Remote File":[[3,"launch-remote-file"]],"Launch with Non-Standard Instance Name":[[3,"launch-with-non-standard-instance-name"]],"Lifespan Events":[[9,"lifespan-events"]],"MessagePack":[[9,"messagepack"]],"Method Filtering":[[9,"method-filtering"]],"Mounting Other Apps":[[9,"mounting-other-apps"]],"OpenAPI Documentation":[[9,"openapi-documentation"]],"Operations":[[7,"operations"]],"Project":[[5,null]],"Quick Start":[[6,null]],"Rate Limiting":[[9,"rate-limiting"]],"Reading Requests":[[6,"reading-requests"]],"Removed":[[2,"removed"]],"Rendering Templates":[[6,"rendering-templates"]],"Request ID":[[9,"request-id"]],"Requests & Responses":[[0,"requests-responses"]],"Responder":[[5,null]],"Responder CLI":[[3,null]],"Reverse Proxy":[[4,"reverse-proxy"]],"Route Groups":[[9,"route-groups"]],"Route Parameters":[[6,"route-parameters"]],"Run the Server":[[6,"run-the-server"]],"Running Locally":[[4,"running-locally"]],"Sending Responses":[[6,"sending-responses"]],"Server-Sent Events (SSE)":[[9,"server-sent-events-sse"]],"Serving Files":[[9,"serving-files"]],"Setup":[[7,"setup"]],"Static Files":[[9,"static-files"]],"Testing":[[8,null]],"Testing Before and After Hooks":[[8,"testing-before-and-after-hooks"]],"Testing Error Handling":[[8,"testing-error-handling"]],"Testing File Uploads":[[8,"testing-file-uploads"]],"Testing Headers and Cookies":[[8,"testing-headers-and-cookies"]],"Testing JSON APIs":[[8,"testing-json-apis"]],"Testing Lifespan Events":[[8,"testing-lifespan-events"]],"Testing Request Validation":[[8,"testing-request-validation"]],"Testing WebSockets":[[8,"testing-websockets"]],"The Idea":[[5,"the-idea"]],"Tips":[[8,"tips"]],"Trusted Hosts":[[9,"trusted-hosts"]],"Type Convertors":[[6,"type-convertors"]],"Unreleased":[[2,"unreleased"]],"User Guide":[[5,null]],"Using Fixtures":[[8,"using-fixtures"]],"Utility Functions":[[0,"utility-functions"]],"Uvicorn Directly":[[4,"uvicorn-directly"]],"Web Service (API) Class":[[0,"module-responder"]],"WebSocket Support":[[9,"websocket-support"]],"What You Get":[[5,"what-you-get"]],"v0.0.1 - 2018-10-12":[[2,"v0-0-1-2018-10-12"]],"v0.0.10 - 2018-10-17":[[2,"v0-0-10-2018-10-17"]],"v0.0.2 - 2018-10-13":[[2,"v0-0-2-2018-10-13"]],"v0.0.3 - 2018-10-13":[[2,"v0-0-3-2018-10-13"]],"v0.0.4 - 2018-10-15":[[2,"v0-0-4-2018-10-15"]],"v0.0.5 - 2018-10-15":[[2,"v0-0-5-2018-10-15"]],"v0.0.6 - 2018-10-16":[[2,"v0-0-6-2018-10-16"]],"v0.0.7 - 2018-10-16":[[2,"v0-0-7-2018-10-16"]],"v0.0.8 - 2018-10-17":[[2,"v0-0-8-2018-10-17"]],"v0.0.9 - 2018-10-17":[[2,"v0-0-9-2018-10-17"]],"v0.1.0 - 2018-10-17":[[2,"v0-1-0-2018-10-17"]],"v0.1.1 - 2018-10-17":[[2,"v0-1-1-2018-10-17"]],"v0.1.2 - 2018-10-18":[[2,"v0-1-2-2018-10-18"]],"v0.1.3 - 2018-10-18":[[2,"v0-1-3-2018-10-18"]],"v0.1.4 - 2018-10-19":[[2,"v0-1-4-2018-10-19"]],"v0.1.5 - 2018-10-20":[[2,"v0-1-5-2018-10-20"]],"v0.1.6 - 2018-10-20":[[2,"v0-1-6-2018-10-20"]],"v0.2.0 - 2018-10-22":[[2,"v0-2-0-2018-10-22"]],"v0.2.1 - 2018-10-23":[[2,"v0-2-1-2018-10-23"]],"v0.2.2 - 2018-10-23":[[2,"v0-2-2-2018-10-23"]],"v0.2.3 - 2018-10-24":[[2,"v0-2-3-2018-10-24"]],"v0.3.0 - 2018-10-24":[[2,"v0-3-0-2018-10-24"]],"v0.3.1 - 2018-10-24":[[2,"v0-3-1-2018-10-24"]],"v0.3.2 - 2018-10-25":[[2,"v0-3-2-2018-10-25"]],"v0.3.3 - 2018-10-25":[[2,"v0-3-3-2018-10-25"]],"v1.0.0 - 2018-10-26":[[2,"v1-0-0-2018-10-26"]],"v1.0.1 - 2018-10-26":[[2,"v1-0-1-2018-10-26"]],"v1.0.2 - 2018-10-27":[[2,"v1-0-2-2018-10-27"]],"v1.0.3 - 2018-10-27":[[2,"v1-0-3-2018-10-27"]],"v1.0.4 - 2018-10-27":[[2,"v1-0-4-2018-10-27"]],"v1.0.5- 2018-10-27":[[2,"v1-0-5-2018-10-27"]],"v1.1.0 - 2018-10-27":[[2,"v1-1-0-2018-10-27"]],"v1.1.1 - 2018-10-29":[[2,"v1-1-1-2018-10-29"]],"v1.1.2 - 2018-11-11":[[2,"v1-1-2-2018-11-11"]],"v1.1.3 - 2019-01-12":[[2,"v1-1-3-2019-01-12"]],"v1.2.0 - 2018-12-29":[[2,"v1-2-0-2018-12-29"]],"v1.3.0 - 2019-02-22":[[2,"v1-3-0-2019-02-22"]],"v1.3.1 - 2019-04-28":[[2,"v1-3-1-2019-04-28"]],"v1.3.2 - 2019-08-15":[[2,"v1-3-2-2019-08-15"]],"v2.0.0 - 2019-09-19":[[2,"v2-0-0-2019-09-19"]],"v2.0.1 - 2019-09-20":[[2,"v2-0-1-2019-09-20"]],"v2.0.2 - 2019-09-20":[[2,"v2-0-2-2019-09-20"]],"v2.0.3 - 2019-09-20":[[2,"v2-0-3-2019-09-20"]],"v2.0.4 - 2019-11-19":[[2,"v2-0-4-2019-11-19"]],"v2.0.5 - 2019-12-15":[[2,"v2-0-5-2019-12-15"]],"v3.0.0 - 2026-03-22":[[2,"v3-0-0-2026-03-22"]]},"docnames":["api","backlog","changes","cli","deployment","index","quickstart","sandbox","testing","tour"],"envversion":{"sphinx":65,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.viewcode":1},"filenames":["api.rst","backlog.md","changes.md","cli.rst","deployment.rst","index.rst","quickstart.rst","sandbox.md","testing.rst","tour.rst"],"indexentries":{"accepts() (responder.request method)":[[0,"responder.Request.accepts",false]],"add_event_handler() (responder.api method)":[[0,"responder.API.add_event_handler",false]],"add_route() (responder.api method)":[[0,"responder.API.add_route",false]],"after_request() (responder.api method)":[[0,"responder.API.after_request",false]],"api (class in responder)":[[0,"responder.API",false]],"apparent_encoding (responder.request property)":[[0,"responder.Request.apparent_encoding",false]],"client (responder.request property)":[[0,"responder.Request.client",false]],"content (responder.request property)":[[0,"responder.Request.content",false]],"content (responder.response attribute)":[[0,"responder.Response.content",false]],"cookies (responder.request property)":[[0,"responder.Request.cookies",false]],"cookies (responder.response attribute)":[[0,"responder.Response.cookies",false]],"encoding (responder.request property)":[[0,"responder.Request.encoding",false]],"exception_handler() (responder.api method)":[[0,"responder.API.exception_handler",false]],"file() (responder.response method)":[[0,"responder.Response.file",false]],"formats (responder.response attribute)":[[0,"responder.Response.formats",false]],"full_url (responder.request property)":[[0,"responder.Request.full_url",false]],"graphql() (responder.api method)":[[0,"responder.API.graphql",false]],"group() (responder.api method)":[[0,"responder.API.group",false]],"headers (responder.request property)":[[0,"responder.Request.headers",false]],"headers (responder.response attribute)":[[0,"responder.Response.headers",false]],"is_100() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_100",false]],"is_200() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_200",false]],"is_300() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_300",false]],"is_400() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_400",false]],"is_500() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_500",false]],"is_json (responder.request property)":[[0,"responder.Request.is_json",false]],"media (responder.response attribute)":[[0,"responder.Response.media",false]],"media() (responder.request method)":[[0,"responder.Request.media",false]],"method (responder.request property)":[[0,"responder.Request.method",false]],"module":[[0,"module-responder",false]],"mount() (responder.api method)":[[0,"responder.API.mount",false]],"on_event() (responder.api method)":[[0,"responder.API.on_event",false]],"params (responder.request property)":[[0,"responder.Request.params",false]],"path_matches_route() (responder.api method)":[[0,"responder.API.path_matches_route",false]],"path_params (responder.request property)":[[0,"responder.Request.path_params",false]],"redirect() (responder.api method)":[[0,"responder.API.redirect",false]],"request (class in responder)":[[0,"responder.Request",false]],"requests (responder.api property)":[[0,"responder.API.requests",false]],"responder":[[0,"module-responder",false]],"response (class in responder)":[[0,"responder.Response",false]],"route() (responder.api method)":[[0,"responder.API.route",false]],"schema() (responder.api method)":[[0,"responder.API.schema",false]],"serve() (responder.api method)":[[0,"responder.API.serve",false]],"session (responder.request property)":[[0,"responder.Request.session",false]],"session (responder.response attribute)":[[0,"responder.Response.session",false]],"session() (responder.api method)":[[0,"responder.API.session",false]],"sse() (responder.response method)":[[0,"responder.Response.sse",false]],"state (responder.request property)":[[0,"responder.Request.state",false]],"status_code (responder.response attribute)":[[0,"responder.Response.status_code",false]],"stream_file() (responder.response method)":[[0,"responder.Response.stream_file",false]],"template() (responder.api method)":[[0,"responder.API.template",false]],"template_string() (responder.api method)":[[0,"responder.API.template_string",false]],"text (responder.request property)":[[0,"responder.Request.text",false]],"url (responder.request property)":[[0,"responder.Request.url",false]],"url_for() (responder.api method)":[[0,"responder.API.url_for",false]]},"objects":{"":[[0,0,0,"-","responder"]],"responder":[[0,1,1,"","API"],[0,1,1,"","Request"],[0,1,1,"","Response"]],"responder.API":[[0,2,1,"","add_event_handler"],[0,2,1,"","add_route"],[0,2,1,"","after_request"],[0,2,1,"","exception_handler"],[0,2,1,"","graphql"],[0,2,1,"","group"],[0,2,1,"","mount"],[0,2,1,"","on_event"],[0,2,1,"","path_matches_route"],[0,2,1,"","redirect"],[0,3,1,"","requests"],[0,2,1,"","route"],[0,2,1,"","schema"],[0,2,1,"","serve"],[0,2,1,"","session"],[0,2,1,"","template"],[0,2,1,"","template_string"],[0,2,1,"","url_for"]],"responder.API.status_codes":[[0,4,1,"","is_100"],[0,4,1,"","is_200"],[0,4,1,"","is_300"],[0,4,1,"","is_400"],[0,4,1,"","is_500"]],"responder.Request":[[0,2,1,"","accepts"],[0,3,1,"","apparent_encoding"],[0,3,1,"","client"],[0,3,1,"","content"],[0,3,1,"","cookies"],[0,3,1,"","encoding"],[0,3,1,"","full_url"],[0,3,1,"","headers"],[0,3,1,"","is_json"],[0,2,1,"","media"],[0,3,1,"","method"],[0,3,1,"","params"],[0,3,1,"","path_params"],[0,3,1,"","session"],[0,3,1,"","state"],[0,3,1,"","text"],[0,3,1,"","url"]],"responder.Response":[[0,5,1,"","content"],[0,5,1,"","cookies"],[0,2,1,"","file"],[0,5,1,"","formats"],[0,5,1,"","headers"],[0,5,1,"","media"],[0,5,1,"","session"],[0,2,1,"","sse"],[0,5,1,"","status_code"],[0,2,1,"","stream_file"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","property","Python property"],"4":["py","function","Python function"],"5":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:method","3":"py:property","4":"py:function","5":"py:attribute"},"terms":{"":[0,2,3,4,5,6,8,9],"0":[3,4,6,9],"1":[0,3,4,6,9],"10":[0,6,9],"100":9,"127":[3,4],"13":[3,4],"150mb":4,"16":3,"2":[4,6,8,9],"200":[3,6,8,9],"201":[6,8],"2024":3,"25":3,"26":3,"3":[4,5,6,8,9],"301":[0,6],"308":2,"3600":9,"4":4,"40":2,"400":[0,8,9],"401":9,"404":6,"41d4":6,"422":[8,9],"429":9,"446655440000":6,"500":[2,6,8,9],"5000":9,"5042":[3,4,6],"55":3,"550e8400":6,"60":9,"600":[0,9],"8":[0,3],"80":4,"8000":[4,6],"8192":0,"9":[5,8],"99":8,"A":[0,3,4,5,6,9],"And":6,"As":9,"But":[6,8,9],"By":[3,6,8,9],"For":[3,4,6,8,9],"If":[0,3,5,6,8,9],"In":[3,6,9],"It":[3,5,6,8,9],"No":[8,9],"Not":6,"On":9,"One":[5,6,8],"That":[4,5,6,8],"The":[0,2,3,4,6,8,9],"There":[8,9],"These":6,"To":3,"Will":0,"With":0,"__main__":[4,5,8],"__name__":[4,5,8,9],"_route_for":2,"a2wsgi":2,"a716":6,"abc123":[8,9],"abf":3,"abil":2,"abl":0,"about":[4,6,8],"abov":[3,5],"accept":[0,2,3,6,8,9],"access":[3,4,9],"acm":3,"acquir":[3,7],"across":[4,9],"activ":7,"actual":[0,6,9],"ad":[1,9],"adapt":[3,9],"add":[0,1,2,6],"add_event_handl":0,"add_head":9,"add_request_id":0,"add_rout":[0,9],"add_tim":[8,9],"add_vers":8,"addit":0,"address":[0,3,6,9],"adher":2,"affect":9,"after":[0,5,6],"after_request":[0,1,8,9],"ag":9,"against":[0,4,9],"agre":6,"ai":9,"alias":2,"alic":9,"all":[0,2,3,4,6,7,8,9],"alloc":8,"allow":9,"allow_credenti":[0,9],"allow_head":[0,9],"allow_method":[0,9],"allow_origin":[0,9],"allow_origin_regex":0,"allowed_host":[0,9],"alongsid":9,"alpin":4,"alreadi":[0,8,9],"also":[0,2,3,6,8,9],"altern":[0,9],"alwai":[4,5,6,9],"an":[0,3,6,9],"angular":9,"ani":[0,4,5,6,8,9],"annot":[5,6,9],"annual":[6,9],"answer":9,"anyon":5,"anyth":6,"anywher":[4,9],"api":[2,3,4,5,6,9],"apispec":2,"app":[0,2,3,4,5,8],"appar":0,"apparent_encod":0,"appear":[5,9],"append":3,"applic":[0,2,4,5,6,8,9],"approach":9,"appropri":9,"approx":3,"ar":[2,3,4,5,6,8,9],"architectur":9,"aren":6,"arg":0,"argument":[0,2,6],"around":[0,5],"arriv":6,"arrow":3,"asgi":[0,2,4,5,6,9],"ask":[5,9],"assert":8,"asset":[3,4,9],"assum":3,"async":[0,1,2,4,5,6,8,9],"asynccontextmanag":9,"asynchron":[2,4],"attach":9,"attack":[4,9],"attr":3,"attribut":[0,3],"auth":[5,9],"auth_check":9,"authent":[6,9],"author":[3,6,7,9],"auto":9,"auto_escap":0,"autobuild":7,"automat":[0,4,5,6,8,9],"avail":[3,6],"avoid":[8,9],"aw":[3,4],"await":[0,6,8,9],"az":3,"azur":[3,4],"b":[6,8],"back":[0,5,6,9],"background":[2,5],"backlog":5,"bad":[8,9],"balanc":4,"bandwidth":9,"bar":9,"base":[0,2,3,5],"base_url":0,"basemodel":[0,8,9],"basic":[3,6],"batteri":5,"battl":5,"becaus":6,"becom":0,"been":3,"befor":[5,6],"before_request":[0,1,2,8,9],"beforehand":3,"behind":[4,9],"being":0,"best":5,"better":5,"bidirect":9,"bin":7,"binari":[6,9],"bind":[0,4,6],"blob":3,"block":[6,8,9],"bodi":[0,6,8,9],"boilerpl":5,"both":[5,6,9],"break":8,"bring":5,"broke":[6,8],"broker":6,"browser":[3,6,7,9],"bucket":[3,9],"buffer":6,"bufix":2,"bug":[2,8],"bugfix":2,"build":[2,4,5,6,7],"built":[2,4,5,6,8,9],"bump":2,"bypass":4,"byte":[0,5,6],"cach":[6,8,9],"caddi":4,"call":[6,8,9],"callabl":0,"came":6,"can":[0,3,4,5,6,8,9],"capac":3,"carri":6,"case":[0,5,6],"cat":9,"catch":9,"cd":7,"ceas":2,"celeri":6,"central":6,"certain":0,"certif":4,"chanc":3,"chang":3,"changelog":5,"channel":9,"chardet":0,"chat":9,"check":[7,8,9],"check_exist":[0,9],"choic":[6,9],"choos":[3,9],"chunk":[0,9],"chunk_siz":0,"ci":2,"circuit":[1,5,9],"class":[2,5,6,8],"clean":[5,9],"cleaner":9,"cleanup":9,"cli":[2,5],"click":5,"client":[0,5,6,8,9],"clone":7,"close":[8,9],"close_database_connection_pool":0,"cloud":[3,5,6],"cmd":4,"code":[0,2,4,6,7,8,9],"collabor":9,"com":[3,6,7,9],"come":[6,9],"command":[3,4],"common":[3,4,6,9],"commonli":6,"commun":9,"compact":9,"compat":3,"complement":[1,9],"complet":9,"compress":[5,6],"comput":6,"concept":[2,9],"concern":6,"conclud":3,"concurr":4,"condit":[8,9],"configur":[2,4,6,8,9],"conflict":2,"confus":8,"connect":[0,4,8,9],"consid":[1,6],"consist":[6,9],"consol":9,"const":9,"consum":9,"contact":0,"contain":[0,4,6],"container":4,"content":[0,2,3,5,6,8,9],"content_typ":[0,8,9],"context":[5,8,9],"contextlib":9,"continu":6,"contract":8,"control":[4,6,9],"convent":4,"convert":[2,6],"convertor":5,"cooki":[0,2,5,6],"copi":4,"cor":[0,2,5,8],"core":[2,4],"coroutin":0,"correctli":4,"correspond":3,"cors_param":[0,9],"count":2,"cpu":[4,6],"creat":[0,5,7,8,9],"create_item":[0,9],"create_pet":9,"cross":9,"css":9,"csv":9,"curl":[3,6],"current":3,"custom":[0,5,6,8],"cycl":6,"d":8,"dashboard":9,"dask":3,"data":[0,2,6,8,9],"databas":[6,8,9],"date":3,"debug":[0,2,9],"decim":6,"declar":[2,6],"decod":[2,9],"decor":[0,6,9],"deep":5,"def":[0,5,6,8,9],"default":[0,1,2,3,6,8,9],"default_valu":[0,9],"defend":9,"defin":[3,6,9],"definit":0,"delet":[6,9],"deliber":6,"denpend":2,"depend":[2,4,8],"deploi":[4,5],"deploy":5,"depth":9,"describ":9,"descript":[0,9],"design":[3,6,9],"detail":[4,9],"detect":[0,5,9],"dev":2,"develop":[4,9],"devic":9,"dict":[0,5,6,9],"dictionari":0,"differ":[3,8,9],"digit":6,"direct":[2,9],"directli":[5,6,8],"directori":[0,3,6,9],"disabl":8,"disk":[0,6,9],"dispatch":[2,9],"distribut":[3,4,9],"django":[5,9],"dna":5,"do":[3,4,6,9],"doc":[7,8,9],"docker":5,"dockerfil":4,"docs_rout":[0,9],"docstr":9,"document":[2,4,5,7],"doesn":[0,9],"domain":9,"don":[3,6,8,9],"done":4,"down":9,"download":9,"dramat":2,"drop":2,"dropbox":3,"dump":2,"duplex":9,"duplic":9,"dure":[4,8],"dynam":6,"e":0,"e29b":6,"each":[0,4,8,9],"easi":6,"echo":3,"ecosystem":[5,6],"edit":[3,7],"editor":9,"effect":8,"effici":4,"either":[0,3],"element":[0,9],"els":[6,9],"email":6,"emb":9,"enabl":[2,9],"enable_async":6,"enable_hst":[0,9],"encod":[0,3],"end":6,"endpoint":[0,2,8,9],"enforc":9,"engin":6,"enough":4,"ensur":9,"enter":8,"entir":[0,9],"entri":3,"entrypoint":5,"enumer":3,"env":4,"environ":[0,4,6],"eol":2,"error":[0,5],"escap":0,"essenti":9,"etc":[6,8,9],"even":[3,4,9],"event":[0,2,5,6],"event_typ":0,"eventsourc":9,"everi":[0,4,5,6,8,9],"everyth":[4,9],"exactli":[8,9],"exampl":[3,6,9],"exc":[0,8,9],"exceed":9,"except":[0,2,5,8,9],"exception_cl":0,"exception_handl":[0,8,9],"exist":[0,2,3,5],"exit":[8,9],"expect":[3,6],"expir":9,"explain":9,"explicit":[6,9],"explicitli":9,"explor":[1,5,9],"export":9,"expos":4,"expose_head":0,"express":[5,6],"ext":[2,9],"extens":[2,6],"extern":6,"extra":[2,3,4,9],"extract":0,"extrem":6,"f":[0,5,6,8,9],"fail":[8,9],"failur":8,"falcon":[5,9],"fall":0,"fals":[0,8,9],"familiar":5,"fast":[4,5,6,8],"fastapi":5,"faster":9,"favourit":3,"featur":[0,5,6],"feed":9,"feel":5,"few":3,"fido":9,"field":[0,8,9],"figur":[5,6],"file":[0,2,4,5,6],"filenam":[0,8],"filepath":6,"filesystem":[2,3],"fill":6,"filter":[5,6],"find":9,"fine":[4,9],"fire":8,"first":9,"fit":5,"fix":7,"fixtur":5,"flag":[4,9],"flask":[5,9],"flask_app":9,"float":[0,6,8],"fly":4,"follow":6,"font":9,"forg":9,"form":[0,3,6],"format":[0,2,6,7,9],"formerli":9,"forward":[4,5,9],"found":[2,6],"foundat":9,"framework":[5,9],"franca":6,"fresh":8,"from":[0,2,3,4,5,6,8,9],"front":4,"frontend":9,"fsspec":3,"full":[0,2,3,6,9],"full_url":[0,6,9],"fulli":6,"fun":5,"func":0,"function":[5,6,9],"fuse":3,"g":[0,3],"game":9,"gatewai":4,"gc":3,"gener":[0,5,8,9],"get":[0,6,9],"get_us":[0,9],"git":[3,7],"github":[3,7],"give":[3,8,9],"given":[0,8],"gmt":3,"go":6,"goe":9,"good":[4,5,9],"googl":[3,4],"grace":4,"grade":6,"gradual":9,"graphen":[0,2,5,9],"graphiql":[2,5,9],"graphql":[0,2,5],"great":[5,9],"greet":[5,9],"greet_world":5,"greetingresourc":9,"group":[0,5],"grow":9,"guard":5,"guid":6,"guido":6,"gzip":[5,6],"h1":6,"ha":[6,8],"hadoop":3,"half":6,"halv":9,"hand":[0,3],"handl":[4,5,6],"handle_value_error":[0,9],"handler":[0,3,5,8,9],"happen":[5,6],"hard":8,"have":[0,2,3,6,8],"hdf":3,"head":3,"header":[0,4,5,6,9],"heavi":6,"hello":[0,3,5,8,9],"hello_html":6,"hello_json":6,"hello_to":6,"hello_world":6,"helloworld":3,"help":0,"here":[4,6,9],"high":[4,9],"hold":[5,6],"home":5,"honor":[4,6],"hood":9,"hook":[1,5,6],"host":[0,2,3,4,5,6],"hostnam":9,"hour":9,"how":[6,9],"hst":5,"html":[0,2,5,6,9],"http":[0,2,3,4,5,6,7,8,9],"httpie":3,"httponli":9,"httpsredirectmiddlewar":4,"httpx":[2,8],"i":[0,2,3,4,5,6,8,9],"id":[0,5,6],"identifi":9,"imag":[4,6,9],"immedi":[5,6,9],"immut":2,"implement":[3,8],"import":[0,2,3,5,6,8,9],"importlib":3,"improv":2,"includ":[3,4,5,6,7,8,9],"incom":[0,6],"incredibli":9,"independ":4,"index":[0,9],"individu":9,"industri":9,"info":[0,2,9],"inform":0,"infrastructur":[4,5],"inherit":[3,9],"initi":[0,8],"inject":9,"input":[8,9],"insensit":[0,5,6],"instal":[0,3,4,7,9],"instanc":[0,4,5,6,8],"instant":6,"instead":[3,6,8,9],"int":[0,6,9],"integ":[0,6],"intens":6,"intent":5,"intention":9,"interact":[2,9],"interfac":[4,6],"intern":[8,9],"internet":3,"invalid":[8,9],"invoc":3,"invok":3,"io":4,"iot":9,"ip":[6,9],"is_100":0,"is_200":0,"is_300":0,"is_400":0,"is_500":0,"is_class_bas":2,"is_json":[0,6],"is_secur":6,"isn":[6,9],"issu":2,"item":[0,8,9],"itemin":0,"itemout":0,"its":[0,2,3,4],"j":9,"javascript":[5,9],"jinja2":[0,6],"joi":5,"jpeg":9,"jpg":9,"json":[0,3,5,6,9],"just":[4,8,9],"keep":[2,6,8,9],"kei":[0,8,9],"kennethreitz":[3,7],"keyword":[0,6],"kind":3,"know":6,"known":0,"kwarg":0,"languag":9,"larg":[5,9],"larger":8,"later":[6,8],"launch":[4,5],"layout":3,"lazili":0,"lead":8,"learn":[5,9],"length":3,"less":9,"let":[4,6,9],"level":4,"librari":[8,9],"licens":0,"lifespan":[0,2,5],"lift":6,"lightn":4,"lightweight":6,"like":[3,4,5,6,8,9],"limit":[1,4,5],"line":[3,6],"lingua":6,"list":[0,3,6,8,9],"list_item":9,"list_pet":9,"list_us":[0,9],"list_users_v2":9,"live":9,"ll":[5,6,9],"load":[0,2,3,4,6,9],"local":5,"localhost":6,"locat":[0,3,6],"log":[4,9],"log_level":2,"log_respons":9,"logic":[5,6,9],"login":9,"long":6,"look":[5,6],"loop":6,"lower":0,"lowercas":6,"machin":9,"mai":4,"main":3,"make":[4,6,8,9],"malici":9,"manag":[5,8,9],"mani":[4,6,9],"manual":0,"map":[2,6,9],"mark":[2,9],"marshmallow":[0,2,9],"match":[0,6,9],"matter":9,"max_ag":[0,9],"mean":[4,6],"mechan":9,"media":[0,2,5,6,8,9],"memori":[0,6,9],"messag":[0,6,9],"messagepack":5,"metadata":6,"method":[0,5,6,8],"microservic":9,"middelwar":0,"middlewar":[1,2,6,9],"might":6,"migrat":[2,9],"mime":0,"mimetyp":[6,9],"min":9,"minim":[3,4],"minimum":2,"minor":2,"miss":[8,9],"mix":9,"ml":9,"mode":[0,7],"model":[0,8,9],"modern":[4,9],"modifi":9,"modul":[2,5,6,9],"more":[3,4,5,6,9],"most":[4,6,8,9],"mount":[0,2,5],"move":[2,6,9],"msgpack":9,"much":[6,9],"multipart":[2,6],"multipl":[2,3,4,9],"multiplay":9,"must":[0,6,9],"mutabl":5,"mutat":[0,5,6],"my_templ":6,"myapi":4,"n":6,"name":[0,5,6,8,9],"namespac":2,"natur":8,"need":[3,4,5,6,8,9],"negoti":[0,5,6,8,9],"nervou":6,"network":8,"never":9,"new":[0,6,8,9],"next":3,"nginx":4,"non":[5,6],"none":[0,2],"normal":9,"notabl":2,"notasecret":0,"note":[6,9],"notic":6,"notif":9,"now":[2,6,9],"npm":3,"number":6,"o":6,"object":[0,2,3,5,6,9],"objecttyp":[0,9],"obviou":8,"oci":3,"oct":3,"off":6,"offload":4,"often":[6,9],"ok":[3,6,8],"on_delet":9,"on_ev":[0,8,9],"on_get":[5,9],"on_post":[5,9],"on_put":9,"on_request":[5,9],"on_startup":8,"onc":9,"one":[0,3,5,6,9],"ones":[6,9],"onli":[6,9],"onmessag":9,"open":[2,6,7],"open_database_connection_pool":0,"openapi":[0,2,5],"openapi_rout":0,"openapi_them":0,"oper":[6,9],"opt":9,"option":[0,2,3,4,5,6],"order":3,"organ":[8,9],"origin":9,"other":[0,2,3,5,6],"otherwis":[0,9],"out":[5,6],"outgo":6,"over":[0,5,6,9],"overal":2,"overhead":8,"overrid":[0,9],"overwhelm":9,"own":[4,9],"p":4,"pace":9,"packag":[2,3,4],"page":[2,6,9],"pagin":6,"painless":8,"param":[0,2,5,6],"paramet":[0,5,8,9],"parameter":0,"pars":[0,2,6,8,9],"part":[6,9],"pass":[0,5,6,8],"passion":5,"password":3,"patch":[6,9],"path":[0,2,3,5,6,8,9],"path_matches_rout":0,"path_param":[0,6],"pattern":[4,5,6,9],"pdf":[6,8,9],"per":[4,8,9],"perfect":9,"period":9,"perman":6,"permit":9,"persist":9,"person":5,"pet":[0,9],"petin":9,"petout":9,"petschema":[0,9],"photo":9,"piec":9,"pin":2,"pip":[3,4,5,7],"place":[5,9],"placehold":6,"plain":[3,6,9],"platform":[2,5,6],"pleas":3,"pleasant":5,"plugin":2,"point":[3,4],"polici":9,"pool":[5,6,9],"popular":[6,9],"port":[0,3,4,6,8],"portion":0,"possibl":6,"post":[0,6,8,9],"potenti":2,"power":[5,8,9],"practic":9,"prefer":9,"prefix":[0,9],"present":6,"prevent":9,"price":[0,8],"primari":0,"print":9,"privat":9,"process":[4,5,6,8,9],"process_data":6,"produc":6,"product":[4,5,6,8,9],"profil":9,"program":3,"programmat":9,"progress":9,"project":[2,3,7],"propag":8,"proper":6,"properli":8,"properti":[0,6,8],"protect":4,"proto":4,"protocol":[3,6,8,9],"prototyp":[2,5],"provid":0,"proxi":5,"public":9,"push":[4,5,9],"put":[6,9],"py":[2,3,4,8],"pydant":[0,8,9],"pyproject":2,"pytest":[5,7,8],"python":[0,2,3,4,5,6,9],"q":6,"queri":[0,5,6,9],"queue":6,"quick":5,"r":[6,8],"race":8,"railwai":4,"rais":[2,8,9],"raise_server_except":8,"random":0,"rang":[0,9],"rapidoc":[0,9],"rare":6,"rate":[1,4,5],"ratelimit":9,"rather":8,"raw":[3,5,6,9],"re":[0,4,5,6,8,9],"react":[2,9],"read":[0,2,5,9],"readabl":9,"readi":[4,5,6],"real":9,"realli":[8,9],"receiv":[0,6,8,9],"receive_byt":9,"receive_incom":6,"receive_json":9,"receive_text":[8,9],"recommend":9,"reconnect":9,"redirect":[0,2,6],"redoc":[0,9],"reduc":2,"ref":3,"refactor":[2,8],"refer":[3,8],"reflect":3,"refus":9,"regist":[0,8,9],"regular":3,"reject":[8,9],"relat":9,"releas":7,"reliabl":8,"remain":9,"remot":5,"remov":9,"renam":8,"render":[0,1,4,5,9],"render_async":6,"replac":[2,6,9],"report":[6,8,9],"repres":0,"represent":0,"req":[0,5,6,8,9],"request":[2,3,4,5],"request_id":[0,9],"request_model":[0,8,9],"requir":[2,8,9],"research":5,"resolv":[2,9],"resolve_hello":[0,9],"resourc":[6,9],"resp":[0,2,5,6,8,9],"respond":[0,2,4,6,7,8,9],"respons":[3,5,8,9],"response_model":[0,9],"rest":[4,5,9],"restrict":9,"result":9,"resum":2,"resume_incomplet":2,"retri":[0,9],"retriev":9,"return":[0,2,5,6,8,9],"revers":5,"rfc3986":2,"right":[5,6,9],"rout":[0,2,5,8],"router":2,"ruff":7,"run":[0,2,3,5,7,8,9],"runner":4,"s3":3,"safe":2,"sai":6,"said":4,"same":[4,5,6,9],"sandbox":5,"sat":3,"scale":5,"schema":[0,2,5,9],"scope":[0,9],"scratch":5,"search":[0,2,6,9],"second":3,"secret":9,"secret_kei":[0,9],"section":[3,9],"secur":9,"see":[3,4,6,8,9],"select":[0,2,3],"self":[0,9],"semant":2,"send":[0,2,5,8,9],"send_byt":9,"send_json":9,"send_text":[8,9],"sent":[0,5,6],"separ":[6,8],"sequenti":2,"serial":[6,9],"serializ":6,"serv":[0,2,4,5,6,8],"server":[0,2,3,4,5,8],"servestat":2,"servic":[3,5,8,9],"session":[0,2,5,6,8],"session_id":9,"set":[0,2,4,5,6,7,8,9],"set_cooki":[2,9],"set_text":0,"setup":[2,8],"sever":9,"sftp":3,"share":[0,5,6,8,9],"short":[1,5,9],"should":5,"shouldn":0,"show":[2,9],"shut":9,"shutdown":[0,4,5,8,9],"shutdwown":2,"side":[6,9],"sign":[5,6,9],"signatur":[6,9],"simpl":[8,9],"simplecooki":0,"simpler":[5,6,9],"simplest":6,"simul":6,"sinc":[6,8],"singl":[2,3,5,6,9],"size":0,"skip":9,"slash":6,"sleep":[6,8],"slim":4,"slow":6,"slowlori":4,"slug":6,"small":[4,5,8,9],"smaller":4,"smb":3,"so":[3,8,9],"solv":9,"some":[4,9],"someon":[6,9],"someth":[5,6,8,9],"sometim":[6,9],"somewher":6,"sourc":[0,7,9],"spawn":4,"spec":[6,9],"special":9,"specif":[0,3,8,9],"specifi":3,"sphinx":7,"spin":6,"sse":[0,5],"ssh":3,"ssl":4,"stabil":2,"standard":[4,5,9],"starlett":[0,2,5,8],"start":[4,5,9],"startup":[0,2,5,8,9],"state":[0,2,8,9],"statement":5,"static":[0,2,4,5,6],"static_dir":[0,9],"static_rout":[0,2,9],"statu":[0,2,6,8,9],"status_cod":[0,5,6,8,9],"stdlib":2,"still":9,"stop":9,"storag":3,"store":[0,3,9],"str":[0,6,8,9],"straightforward":9,"stranger":[0,9],"stream":[0,2,9],"stream_fil":[0,9],"strict":9,"string":[0,5,6,9],"strip":9,"structur":9,"style":9,"stylesheet":9,"subcommand":3,"subrout":[5,9],"subsequ":9,"subtl":2,"success":6,"successor":4,"suit":8,"suppli":2,"support":[0,1,2,3,4,5,6],"surpris":3,"suspect":5,"swagger":[5,9],"swagger_ui":[0,9],"switch":2,"symbol":3,"sync":[2,5,6],"syntax":[5,6],"synthet":3,"system":[3,6,9],"t":[0,3,4,6,8,9],"take":[5,6],"tamper":9,"target":[2,3],"task":[2,5],"tear":9,"tell":[6,9],"templat":[0,1,2,5],"template_str":[0,6],"templates_dir":0,"termin":4,"terms_of_servic":0,"test":[0,2,5,7],"test_500":[2,8],"test_api":8,"test_create_item":8,"test_custom_error":8,"test_head":8,"test_hello":8,"test_hook":8,"test_json":8,"test_upload":8,"test_valid":8,"test_websocket":8,"test_with_lifespan":8,"testclient":[0,2,8],"text":[0,3,5,6,8,9],"than":[3,8,9],"thei":[6,9],"them":[5,6,8,9],"theme":[0,9],"themselv":9,"thi":[0,2,3,4,6,8,9],"thing":[6,8],"think":6,"those":3,"though":4,"thousand":4,"thread":[5,6],"threadpoolexecutor":2,"three":[6,9],"through":[6,9],"throughput":9,"time":[0,3,5,6,8,9],"time_start":0,"timeout":4,"tip":5,"titl":[0,6,9],"tl":4,"togeth":[5,9],"token":[6,9],"toml":2,"too":[4,8,9],"tool":7,"toolbelt":2,"total":9,"tour":5,"trace":9,"traceback":[2,9],"tradit":9,"traffic":4,"transfer":3,"transport":9,"treat":[0,9],"trick":9,"trigger":8,"true":[0,6,8,9],"trust":5,"trustedhostmiddlewar":4,"tupl":[0,6,8],"two":[6,9],"type":[0,2,3,5,9],"typic":[3,6,9],"typo":[2,9],"ui":[2,5,9],"unauthor":9,"under":[6,9],"understand":6,"unicod":0,"uniqu":9,"unknown":0,"unless":9,"unlik":9,"unmaintain":2,"unmatch":9,"unpin":2,"unrecogn":9,"until":6,"up":[0,2,6,7,9],"updat":[2,6,9],"upgrad":[2,7,9],"upload":[2,5,6],"url":[0,2,3,5,6,8,9],"url_for":[0,8],"urllib":2,"us":[0,2,3,4,5,6,9],"usag":[0,2],"user":[0,3,6,9],"user_id":[6,9],"usernam":9,"usual":[5,8],"utf":[0,3],"util":5,"uuid":[0,6,9],"uuid4":0,"uv":[3,5,7],"uvicorn":[0,2,3,5,6],"uvloop":4,"v1":[0,9],"v2":9,"valid":[3,5,6,9],"valu":[0,5,6,8,9],"valueerror":[0,8,9],"variabl":[0,4,6],"ve":[5,8],"venv":7,"verb":9,"veri":[0,6],"verifi":8,"version":[0,2,3,8,9],"via":[2,9],"view":[0,2,5,6,8],"virtual":4,"virtualenv":7,"visit":9,"vue":9,"w":[8,9],"wa":[0,2,6],"wai":[4,6,9],"walk":[6,9],"want":[0,4,5,6,8,9],"watch":7,"web":[3,4,5,9],"webdav":3,"websit":9,"websocket":[0,1,2,4,5],"websocket_connect":8,"well":5,"went":5,"wget":3,"what":[6,8,9],"whatev":6,"when":[2,3,4,5,6,8,9],"where":[3,4,6,9],"whether":[0,6],"which":[2,3,6,8,9],"whichev":5,"while":[6,9],"whitenois":2,"who":[5,6],"widget":8,"wildcard":9,"window":2,"wire":6,"within":[2,3],"without":[0,4,6,8,9],"won":[6,8,9],"work":[3,4,5,6,9],"workdir":4,"worker":4,"workgroup":3,"world":[0,3,5,8,9],"worri":8,"would":[3,8],"wrap":[6,8,9],"write":[6,9],"wrong":9,"wsgi":[0,2,4,5,9],"x":[0,4,6,8,9],"x89png":6,"xml":0,"xss":9,"yaml":[0,2,5,6,8,9],"yield":[0,9],"yml":[0,9],"you":[0,3,4,6,8,9],"your":[3,4,6,8,9],"yourself":6,"zero":4},"titles":["API Documentation","Backlog","Changelog","Responder CLI","Deployment","Responder","Quick Start","Development Sandbox","Testing","Feature Tour"],"titleterms":{"0":2,"01":2,"02":2,"03":2,"04":2,"08":2,"09":2,"1":2,"10":2,"11":2,"12":2,"13":2,"15":2,"16":2,"17":2,"18":2,"19":2,"2":2,"20":2,"2018":2,"2019":2,"2026":2,"22":2,"23":2,"24":2,"25":2,"26":2,"27":2,"28":2,"29":2,"3":2,"4":2,"5":2,"6":2,"7":2,"8":2,"9":2,"The":5,"ad":2,"after":[8,9],"api":[0,8],"app":9,"applic":3,"background":6,"backlog":1,"base":9,"befor":[8,9],"build":3,"chang":2,"changelog":2,"class":[0,9],"cli":3,"cloud":4,"convertor":6,"cooki":[8,9],"cor":9,"creat":6,"custom":9,"deploy":4,"deprec":2,"develop":7,"directli":4,"docker":4,"document":[0,9],"entrypoint":3,"error":[8,9],"event":[8,9],"featur":9,"file":[3,8,9],"filter":9,"fix":2,"fixtur":8,"function":0,"futur":1,"get":[5,8],"graphql":9,"group":9,"guid":5,"handl":[8,9],"header":8,"hello":6,"hook":[8,9],"host":9,"hst":9,"id":9,"idea":[1,5],"instal":5,"instanc":3,"javascript":3,"json":8,"launch":3,"lifespan":[8,9],"limit":9,"local":[3,4],"messagepack":9,"method":9,"modul":3,"mount":9,"name":3,"non":3,"openapi":9,"oper":7,"other":9,"paramet":6,"platform":4,"project":5,"proxi":4,"quick":6,"rate":9,"read":6,"remot":3,"remov":2,"render":6,"request":[0,6,8,9],"respond":[3,5],"respons":[0,6],"revers":4,"rout":[6,9],"run":[4,6],"sandbox":7,"send":6,"sent":9,"serv":9,"server":[6,9],"servic":[0,6],"session":9,"setup":7,"sse":9,"standard":3,"start":[6,8],"static":9,"support":9,"task":6,"templat":6,"test":8,"tip":8,"tour":9,"trust":9,"type":6,"unreleas":2,"upload":8,"us":8,"user":5,"util":0,"uvicorn":4,"v0":2,"v1":2,"v2":2,"v3":2,"valid":8,"view":9,"web":[0,6],"websocket":[8,9],"what":5,"world":6,"you":5}}) \ No newline at end of file diff --git a/testing.html b/testing.html index f146d38..af5c29c 100644 --- a/testing.html +++ b/testing.html @@ -303,7 +303,7 @@ network overhead. Avoid API() configuration (like cors=True), create a new instance in the test rather than sharing the fixture.

    -
  • Use ``api.url_for()`` instead of hard-coded paths. It’s a small +

  • Use api.url_for() instead of hard-coded paths. It’s a small thing, but it makes refactoring painless.

  • Test the contract, not the implementation. Assert on status codes, response bodies, and headers — not on internal state.

  • diff --git a/tour.html b/tour.html index 920ae01..37d5e51 100644 --- a/tour.html +++ b/tour.html @@ -42,12 +42,24 @@

    Feature Tour

    -

    This section walks through Responder’s features in detail. Each section -includes working code examples you can copy into your application.

    +

    This section walks through Responder’s features in depth. Each section +explains the concept, shows working code, and explains the design choices +behind it. If you’re new to web development, this is a good place to learn +how modern web frameworks work under the hood.

    Method Filtering

    -

    By default, a route matches all HTTP methods. If you want to restrict a -route to specific methods, pass the methods parameter:

    +

    HTTP defines several methods (also called verbs) that describe what a +client wants to do with a resource. The most common are:

    +
      +
    • GET — retrieve data

    • +
    • POST — create something new

    • +
    • PUT — replace something entirely

    • +
    • PATCH — update part of something

    • +
    • DELETE — remove something

    • +
    +

    By default, a Responder route matches all methods. This is fine for simple +endpoints, but REST APIs typically map different methods to different +operations. Use the methods parameter to restrict a route:

    @api.route("/items", methods=["GET"])
     def list_items(req, resp):
         resp.media = {"items": []}
    @@ -58,13 +70,19 @@ route to specific methods, pass the <
         resp.media = {"created": data}
     
    -

    Note the check_existing=False — this allows you to register multiple -handlers for the same path with different methods.

    +

    Note the check_existing=False — Responder normally prevents you from +registering two routes with the same path (to catch typos). When you +intentionally want multiple handlers for the same path with different +methods, you need to opt in.

    Class-Based Views

    -

    For more complex resources, you can use class-based views. Responder will -dispatch to the appropriate method handler based on the HTTP method:

    +

    Function-based views are great for simple endpoints, but sometimes you want +to group related HTTP methods together into a single resource. This is +where class-based views come in — a pattern popularized by +Falcon.

    +

    Responder dispatches to the appropriate method handler based on the HTTP +method:

    @api.route("/{greeting}")
     class GreetingResource:
         def on_get(self, req, resp, *, greeting):
    @@ -81,13 +99,17 @@ dispatch to the appropriate method handler based on the HTTP method:

    The on_request method is called for all HTTP methods, much like middleware scoped to a single route. Method-specific handlers (on_get, on_post, on_put, on_delete, etc.) are called after.

    -

    No inheritance required — just define a class with the right method names.

    +

    No inheritance required — just define a class with the right method names. +This is simpler than Django’s View classes and more Pythonic than +framework-specific base classes.

    Lifespan Events

    -

    Modern applications often need to set up resources on startup (database -connections, caches, ML models) and tear them down on shutdown. Responder -supports the lifespan context manager pattern:

    +

    Real applications need to set up resources when they start (database +connection pools, ML models, caches) and tear them down when they stop. +This is called the application lifespan.

    +

    The modern approach is the context manager pattern, where startup and +shutdown are two halves of the same block:

    from contextlib import asynccontextmanager
     
     @asynccontextmanager
    @@ -101,7 +123,10 @@ supports the lifespan context manager pattern:

    api = responder.API(lifespan=lifespan)
    -

    You can also use the traditional event decorator style:

    +

    Everything before yield runs at startup. Everything after runs at +shutdown. If startup fails, the server won’t start. If shutdown raises, +it’s logged but the server still exits.

    +

    The traditional event decorator style also works:

    @api.on_event("startup")
     async def startup():
         print("starting up")
    @@ -111,52 +136,65 @@ supports the lifespan context manager pattern:

    print("shutting down")
    -

    The context manager approach is preferred for new code — it makes the -startup/shutdown relationship explicit and keeps related code together.

    +

    The context manager is preferred for new code — it keeps related startup +and shutdown logic together and makes resource cleanup more explicit.

    Serving Files

    -

    Serve files from disk with automatic content-type detection. Responder -uses Python’s mimetypes module to figure out the right Content-Type -header for you:

    +

    Web applications often need to serve files — downloads, reports, images. +Responder makes this simple with resp.file(), which reads a file from +disk and sets the Content-Type header automatically using Python’s +mimetypes module:

    @api.route("/download")
     def download(req, resp):
         resp.file("reports/annual.pdf")
     
    -

    You can override the content type if needed:

    +

    You can override the content type if the automatic detection isn’t right:

    @api.route("/image")
     def image(req, resp):
         resp.file("photos/cat.jpg", content_type="image/jpeg")
     
    +

    For large files, use resp.stream_file() to avoid loading the entire +file into memory. This streams the file in chunks:

    +
    @api.route("/export")
    +def export(req, resp):
    +    resp.stream_file("data/export.csv")
    +
    +

    Custom Error Handling

    -

    By default, unhandled exceptions result in a 500 Internal Server Error. -You can register custom handlers for specific exception types to return -structured error responses:

    +

    In production, you don’t want your users to see raw Python tracebacks. +Responder lets you register custom handlers for specific exception types, +so you can return clean, structured error responses:

    @api.exception_handler(ValueError)
     async def handle_value_error(req, resp, exc):
         resp.status_code = 400
         resp.media = {"error": str(exc)}
     
    -

    Now, any route that raises a ValueError will return a clean 400 response -with a JSON error message instead of a generic 500 page.

    +

    Now, any route that raises a ValueError will return a clean JSON +response with a 400 status code instead of a generic 500 error page.

    +

    This is a common pattern in API development — you define your own exception +classes for different error conditions, register handlers for each, and +your API always returns consistent, machine-readable error responses.

    Before-Request Hooks

    -

    Run code before every request. This is useful for logging, adding common -headers, or setting up per-request state:

    +

    Sometimes you need to run the same code before every request — +authentication checks, request logging, adding common headers, or setting +up per-request state. Before-request hooks let you do this without +duplicating code in every route:

    @api.route(before_request=True)
     def add_headers(req, resp):
    -    resp.headers["X-API-Version"] = "3.1"
    +    resp.headers["X-API-Version"] = "3.2"
     
    -

    Short-circuiting: If your hook sets resp.status_code, the route -handler will be skipped entirely and the response will be sent immediately. -This is the pattern for authentication guards:

    +

    Short-circuiting is the really powerful part. If your hook sets +resp.status_code, the route handler is skipped entirely and the +response is sent immediately. This is the pattern for authentication:

    @api.route(before_request=True)
     def auth_check(req, resp):
         if "Authorization" not in req.headers:
    @@ -165,17 +203,32 @@ This is the pattern for authentication guards:

    If the Authorization header is missing, the client gets a 401 response -and the actual route handler never runs.

    -

    WebSocket hooks work the same way:

    -
    @api.before_request(websocket=True)
    -async def ws_auth(ws):
    -    await ws.accept()
    +and the actual route handler never runs. This is cleaner than adding
    +auth checks to every individual route.

    +
    +
    +

    After-Request Hooks

    +

    The complement to before-request hooks. After-request hooks run after the +route handler completes but before the response is sent. They’re useful +for logging, adding response headers, or any post-processing:

    +
    @api.after_request()
    +def log_response(req, resp):
    +    print(f"{req.method} {req.full_url} -> {resp.status_code}")
    +
    +@api.after_request()
    +async def add_timing(req, resp):
    +    resp.headers["X-Served-By"] = "responder"
     

    WebSocket Support

    -

    Responder supports WebSockets for real-time, bidirectional communication:

    +

    HTTP is a request-response protocol — the client asks, the server answers. +But some applications need real-time, bidirectional communication: chat +apps, live dashboards, multiplayer games, collaborative editors.

    +

    WebSockets solve this by +upgrading an HTTP connection into a persistent, full-duplex channel where +both sides can send messages at any time:

    @api.route("/ws", websocket=True)
     async def websocket(ws):
         await ws.accept()
    @@ -187,13 +240,49 @@ and the actual route handler never runs.

    You can send and receive in multiple formats:

      -
    • send_text / receive_text — plain text

    • -
    • send_json / receive_json — JSON objects

    • +
    • send_text / receive_text — plain text strings

    • +
    • send_json / receive_json — JSON objects (auto-serialized)

    • send_bytes / receive_bytes — raw binary data

    +

    WebSocket routes are marked with websocket=True in the route decorator. +They receive a ws object instead of req and resp.

    +
    +
    +

    Server-Sent Events (SSE)

    +

    SSE is a simpler alternative to WebSockets for one-way real-time +communication — the server pushes events to the client, but the client +can’t send messages back. This is perfect for live feeds, progress bars, +notification streams, and AI response streaming.

    +

    Unlike WebSockets, SSE works over plain HTTP, is automatically reconnected +by the browser, and doesn’t require any special client-side libraries:

    +
    @api.route("/events")
    +async def events(req, resp):
    +    @resp.sse
    +    async def stream():
    +        for i in range(10):
    +            yield {"data": f"message {i}"}
    +
    +
    +

    On the client side, you consume SSE events with JavaScript’s built-in +EventSource API:

    +
    const source = new EventSource("/events");
    +source.onmessage = (event) => {
    +    console.log(event.data);
    +};
    +
    +
    +

    Each yielded value can be a string (treated as data) or a dict with the +standard SSE fields:

    +
    yield {"event": "update", "data": "hello", "id": "1", "retry": "5000"}
    +yield "simple string message"
    +
    +

    GraphQL

    +

    GraphQL is a query language for APIs that lets +clients request exactly the data they need — no more, no less. Instead of +multiple REST endpoints, you define a schema and let clients query it.

    Responder includes built-in GraphQL support via Graphene. Set up a full GraphQL endpoint with a single method call:

    @@ -208,16 +297,20 @@ with a single method call:

    api.graphql("/graphql", schema=graphene.Schema(query=Query)) -

    Visiting /graphql in a browser renders the GraphiQL interactive IDE, -where you can explore your schema and test queries. Programmatic clients -can POST JSON queries to the same endpoint.

    +

    Visiting /graphql in a browser renders the +GraphiQL interactive IDE, where +you can explore your schema, write queries, and see results in real-time. +Programmatic clients can POST JSON queries to the same endpoint.

    You can access the Responder request and response objects in your resolvers through info.context["request"] and info.context["response"].

    OpenAPI Documentation

    -

    Responder can generate an OpenAPI schema and serve interactive API -documentation automatically:

    +

    OpenAPI (formerly Swagger) is the industry +standard for describing REST APIs. An OpenAPI specification lets you +auto-generate interactive documentation, client libraries, and validation +logic.

    +

    Responder generates OpenAPI specs from your code:

    api = responder.API(
         title="Pet Store",
         version="1.0",
    @@ -232,9 +325,11 @@ documentation automatically:

  • Interactive Swagger UI documentation at /docs

  • There are three ways to document your endpoints.

    -

    Pydantic models — the recommended approach for new APIs. Use -request_model and response_model to annotate your routes, and -Responder will generate the schema automatically:

    +

    Pydantic models — the recommended approach. Use request_model and +response_model to annotate your routes, and Responder generates the +schema automatically. When request_model is set, request bodies are +also validated automatically — invalid inputs get a 422 response with +detailed error messages:

    from pydantic import BaseModel
     
     class PetIn(BaseModel):
    @@ -253,18 +348,10 @@ Responder will generate the schema automatically:

    resp.media = {"id": 1, **data}
    -

    This generates a full OpenAPI path with requestBody and responses -schemas, all linked by $ref to your Pydantic models in -components/schemas.

    -

    You can also register standalone schemas with the @api.schema decorator:

    -
    @api.schema("Pet")
    -class Pet(BaseModel):
    -    name: str
    -    age: int = 0
    -
    -
    -

    YAML docstrings — inline your OpenAPI spec directly in the docstring. -This gives you full control over every detail:

    +

    When response_model is set, the response is serialized through the +model — extra fields are stripped and types are enforced.

    +

    YAML docstrings — for full control, embed OpenAPI YAML in the +docstring:

    @api.route("/pets")
     def list_pets(req, resp):
         """A list of pets.
    @@ -278,8 +365,7 @@ This gives you full control over every detail:

    resp.media = [{"name": "Fido"}]
    -

    Marshmallow schemas — if you’re already using marshmallow for -validation, Responder integrates with it via the apispec plugin:

    +

    Marshmallow schemas — if you’re already using marshmallow:

    from marshmallow import Schema, fields
     
     @api.schema("Pet")
    @@ -287,16 +373,39 @@ validation, Responder integrates with it via the apispec plugin:

    name = fields.Str()
    -

    All three approaches can be mixed in the same API. Pydantic models, -marshmallow schemas, and YAML docstrings all contribute to the same -generated OpenAPI specification.

    -

    You can choose from multiple documentation themes: -swagger_ui (default), redoc, rapidoc, or elements.

    +

    All three approaches can be mixed in the same API. You can choose from +multiple documentation themes: swagger_ui (default), redoc, +rapidoc, or elements.

    +
    +
    +

    Route Groups

    +

    As your application grows, you’ll want to organize routes logically. +Route groups let you share a URL prefix across related endpoints — a +common pattern for API versioning:

    +
    v1 = api.group("/v1")
    +
    +@v1.route("/users")
    +def list_users(req, resp):
    +    resp.media = []
    +
    +@v1.route("/users/{user_id:int}")
    +def get_user(req, resp, *, user_id):
    +    resp.media = {"id": user_id}
    +
    +v2 = api.group("/v2")
    +
    +@v2.route("/users")
    +def list_users_v2(req, resp):
    +    resp.media = {"users": [], "total": 0}
    +
    +
    +

    This keeps your code organized without affecting the routing logic.

    Mounting Other Apps

    -

    Responder can mount any WSGI or ASGI application at a subroute. This means -you can gradually migrate from Flask, or run multiple frameworks side by side:

    +

    Responder can mount any WSGI or ASGI application at a subroute. This is +incredibly useful for gradual migrations — you can run Flask and Responder +side by side, moving routes over one at a time:

    from flask import Flask
     
     flask_app = Flask(__name__)
    @@ -310,10 +419,14 @@ you can gradually migrate from Flask, or run multiple frameworks side by side:
     

    Requests to /flask/ will be handled by Flask. Everything else goes through Responder. Both WSGI and ASGI apps are supported — Responder -wraps WSGI apps automatically.

    +wraps WSGI apps in an ASGI adapter automatically.

    Cookies

    +

    Cookies are +small pieces of data that the server asks the browser to store and send +back with every subsequent request. They’re the foundation of sessions, +authentication tokens, and user preferences on the web.

    Reading and writing cookies is straightforward:

    # Read cookies from the request
     session_id = req.cookies.get("session_id")
    @@ -322,24 +435,27 @@ wraps WSGI apps automatically.

    resp.cookies["hello"] = "world"
    -

    For more control over cookie directives, use set_cookie:

    +

    For production use, you’ll want to set security directives. The +httponly flag prevents JavaScript from reading the cookie (defending +against XSS attacks), and secure ensures it’s only sent over HTTPS:

    resp.set_cookie(
         "token",
         value="abc123",
    -    max_age=3600,
    -    secure=True,
    -    httponly=True,
    +    max_age=3600,        # expires in 1 hour
    +    secure=True,         # HTTPS only
    +    httponly=True,        # no JavaScript access
         path="/",
     )
     
    -

    Supported directives: key, value, expires, max_age, -domain, path, secure, httponly.

    Static Files

    -

    Static files are served from the static/ directory by default:

    +

    Most web applications serve static assets — CSS stylesheets, JavaScript +files, images, fonts. Responder serves these from the static/ directory +by default:

    api = responder.API(static_dir="static", static_route="/static")
     
    -

    Place your CSS, JavaScript, images, and other assets in the static/ -directory and they’ll be served automatically.

    -

    For single-page applications, you can serve index.html as the default -response for all unmatched routes:

    +

    Place your assets in the static/ directory and they’ll be served +automatically at /static/style.css, /static/app.js, etc.

    +

    For single-page applications (React, Vue, Angular), you can serve +index.html as the default response for all unmatched routes:

    api.add_route("/", static=True)
     
    -

    You can add additional static directories at runtime:

    -
    api.static_app.add_directory("extra_assets")
    -
    -

    CORS

    -

    Enable Cross-Origin Resource Sharing for your API:

    +

    CORS (Cross- +Origin Resource Sharing) is a security mechanism that controls which +websites can make requests to your API. Browsers enforce this — if your +API is at api.example.com and your frontend is at app.example.com, +the browser will block requests unless your API explicitly allows it.

    +

    Enable CORS and configure which origins are allowed:

    api = responder.API(cors=True, cors_params={
    -    "allow_origins": ["https://example.com"],
    +    "allow_origins": ["https://app.example.com"],
         "allow_methods": ["GET", "POST"],
         "allow_headers": ["*"],
         "allow_credentials": True,
    @@ -390,98 +506,54 @@ response for all unmatched routes:

    })
    -

    The default CORS policy is restrictive — you must explicitly enable the -origins, methods, and headers your frontend needs.

    +

    The default policy is restrictive — you must explicitly allow each origin. +Using ["*"] for allow_origins permits any website to call your API, +which is fine for public APIs but not for private ones.

    HSTS

    -

    Force all traffic to HTTPS with a single flag:

    +

    HSTS +(HTTP Strict Transport Security) tells browsers to always use HTTPS when +communicating with your server. Once a browser sees the HSTS header, it +will refuse to connect over plain HTTP, even if the user types http:// +in the address bar:

    api = responder.API(enable_hsts=True)
     
    -

    This adds the Strict-Transport-Security header and redirects HTTP -requests to HTTPS.

    Trusted Hosts

    -

    Protect against HTTP Host header attacks by restricting which hostnames -your application will respond to:

    +

    The Host header in an HTTP request tells the server which domain name +the client used. Attackers can forge this header to trick your application +into generating URLs to malicious domains (a class of attack called Host +header injection).

    +

    Restrict which hostnames your application accepts:

    api = responder.API(allowed_hosts=["example.com", "*.example.com"])
     
    -

    Requests with a Host header that doesn’t match any of the patterns -will receive a 400 Bad Request response. Wildcard domains are supported.

    -

    By default, all hostnames are allowed.

    -
    -
    -

    Server-Sent Events (SSE)

    -

    Stream real-time updates to the client using Server-Sent Events. This is -great for live feeds, progress updates, and AI streaming responses:

    -
    @api.route("/events")
    -async def events(req, resp):
    -    @resp.sse
    -    async def stream():
    -        for i in range(10):
    -            yield {"data": f"message {i}"}
    -
    -
    -

    Each yielded value can be a string (treated as data) or a dict with -data, event, id, and retry fields:

    -
    yield {"event": "update", "data": "hello", "id": "1"}
    -yield "simple string message"
    -
    -
    -
    -
    -

    Streaming Files

    -

    For large files, use resp.stream_file() to stream the content without -loading the entire file into memory:

    -
    @api.route("/download")
    -def download(req, resp):
    -    resp.stream_file("large-dataset.csv")
    -
    -
    -

    For small files where memory isn’t a concern, resp.file() loads the -entire file at once — simpler but less efficient for large files.

    -
    -
    -

    After-Request Hooks

    -

    Run code after every request, useful for logging, adding headers, or -cleanup:

    -
    @api.after_request()
    -def log_response(req, resp):
    -    print(f"{req.method} {req.full_url} -> {resp.status_code}")
    -
    -
    -
    -
    -

    Route Groups

    -

    Organize related routes with a shared URL prefix. Useful for API versioning -and logical grouping:

    -
    v1 = api.group("/v1")
    -
    -@v1.route("/users")
    -def list_users(req, resp):
    -    resp.media = []
    -
    -@v1.route("/users/{user_id:int}")
    -def get_user(req, resp, *, user_id):
    -    resp.media = {"id": user_id}
    -
    -
    +

    Requests with unrecognized hosts get a 400 Bad Request. Wildcard +patterns are supported. By default, all hostnames are allowed.

    Request ID

    -

    Auto-generate unique request IDs for tracing and debugging. If the client -sends an X-Request-ID header, it’s forwarded; otherwise a new UUID is -generated:

    +

    In distributed systems, tracing a single request across multiple services +is essential for debugging. Request IDs are unique identifiers attached to +each request — if something goes wrong, you can search your logs for that +ID and find every related event.

    +

    Responder can auto-generate request IDs. If the client sends an +X-Request-ID header (common in microservice architectures), it’s +forwarded. Otherwise, a new UUID is generated:

    api = responder.API(request_id=True)
     
    +

    The ID appears in the X-Request-ID response header.

    Rate Limiting

    -

    Built-in token bucket rate limiter:

    +

    Rate limiting prevents individual clients from overwhelming your API with +too many requests. It’s essential for public APIs, and good practice even +for internal ones.

    +

    Responder includes a built-in token bucket rate limiter:

    from responder.ext.ratelimit import RateLimiter
     
     limiter = RateLimiter(requests=100, period=60)  # 100 req/min
    @@ -489,19 +561,24 @@ generated:

    When the limit is exceeded, clients receive a 429 Too Many Requests -response with Retry-After and X-RateLimit-Remaining headers.

    +response with a Retry-After header. Every response includes +X-RateLimit-Limit and X-RateLimit-Remaining headers so clients +can pace themselves.

    +

    The rate limiter is per-client, keyed by IP address.

    MessagePack

    -

    In addition to JSON and YAML, Responder supports MessagePack for efficient -binary serialization:

    -
    # Decode MessagePack request body
    +

    MessagePack is a binary serialization format +that’s more compact and faster to parse than JSON. It’s useful for +high-throughput APIs, IoT devices, and anywhere bandwidth matters.

    +

    Responder supports MessagePack alongside JSON and YAML:

    +
    # Decode a MessagePack request body
     data = await req.media("msgpack")
    -
    -# Content negotiation also works — clients can send
    -# Accept: application/x-msgpack to receive MessagePack responses.
     
    +

    Content negotiation works too — clients can send +Accept: application/x-msgpack to receive MessagePack responses +instead of JSON.

    @@ -535,9 +612,12 @@ binary serialization:

  • Serving Files
  • Custom Error Handling
  • Before-Request Hooks
  • +
  • After-Request Hooks
  • WebSocket Support
  • +
  • Server-Sent Events (SSE)
  • GraphQL
  • OpenAPI Documentation
  • +
  • Route Groups
  • Mounting Other Apps
  • Cookies
  • Cookie-Based Sessions
  • @@ -545,10 +625,6 @@ binary serialization:

  • CORS
  • HSTS
  • Trusted Hosts
  • -
  • Server-Sent Events (SSE)
  • -
  • Streaming Files
  • -
  • After-Request Hooks
  • -
  • Route Groups
  • Request ID
  • Rate Limiting
  • MessagePack