diff --git a/.buildinfo b/.buildinfo index b2e6262..a121562 100644 --- a/.buildinfo +++ b/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file records the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 48bd624a1e2c91349b6c4e0ee21cd6e5 +config: e5a35904bc7bb78a12349affaec49f9c tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.doctrees/api.doctree b/.doctrees/api.doctree index 4898908..cd61576 100644 Binary files a/.doctrees/api.doctree and b/.doctrees/api.doctree differ diff --git a/.doctrees/changes.doctree b/.doctrees/changes.doctree index 7ef336f..afee43e 100644 Binary files a/.doctrees/changes.doctree and b/.doctrees/changes.doctree differ diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle index 6d7a69d..492732d 100644 Binary files a/.doctrees/environment.pickle and b/.doctrees/environment.pickle differ diff --git a/.doctrees/guide-config.doctree b/.doctrees/guide-config.doctree new file mode 100644 index 0000000..fab8f65 Binary files /dev/null and b/.doctrees/guide-config.doctree differ diff --git a/.doctrees/index.doctree b/.doctrees/index.doctree index 90c799e..aaba530 100644 Binary files a/.doctrees/index.doctree and b/.doctrees/index.doctree differ diff --git a/.doctrees/quickstart.doctree b/.doctrees/quickstart.doctree index 1e297e3..586a5ae 100644 Binary files a/.doctrees/quickstart.doctree and b/.doctrees/quickstart.doctree differ diff --git a/.doctrees/tour.doctree b/.doctrees/tour.doctree index 05defef..8e731dc 100644 Binary files a/.doctrees/tour.doctree and b/.doctrees/tour.doctree differ diff --git a/.doctrees/tutorial-auth.doctree b/.doctrees/tutorial-auth.doctree new file mode 100644 index 0000000..3ee57a1 Binary files /dev/null and b/.doctrees/tutorial-auth.doctree differ diff --git a/.doctrees/tutorial-middleware.doctree b/.doctrees/tutorial-middleware.doctree new file mode 100644 index 0000000..7d702be Binary files /dev/null and b/.doctrees/tutorial-middleware.doctree differ diff --git a/.doctrees/tutorial-websockets.doctree b/.doctrees/tutorial-websockets.doctree new file mode 100644 index 0000000..27a0a2c Binary files /dev/null and b/.doctrees/tutorial-websockets.doctree differ diff --git a/_modules/index.html b/_modules/index.html index 394b367..ef1a4f1 100644 --- a/_modules/index.html +++ b/_modules/index.html @@ -4,13 +4,13 @@ - Overview: module code — responder 3.2.0 documentation + Overview: module code — responder 3.4.1 documentation - + @@ -39,6 +39,8 @@

All modules for which code is available

diff --git a/_modules/responder/api.html b/_modules/responder/api.html index f175740..618dbe8 100644 --- a/_modules/responder/api.html +++ b/_modules/responder/api.html @@ -4,13 +4,13 @@ - responder.api — responder 3.2.0 documentation + responder.api — responder 3.4.1 documentation - + @@ -103,6 +103,31 @@ lifespan=None, request_id=False, ): + """Create a new Responder API instance. + + :param debug: If ``True``, enable debug mode with verbose error pages. + :param title: The title of the API, used in OpenAPI documentation. + :param version: The version string for the API (e.g. ``"1.0"``). + :param description: A longer description of the API for OpenAPI docs. + :param terms_of_service: URL to the API's terms of service. + :param contact: Contact information dict for the API (``name``, ``url``, ``email``). + :param license: License information dict (``name``, ``url``). + :param openapi: The OpenAPI version string (e.g. ``"3.0.2"``). Enables OpenAPI schema generation. + :param openapi_route: The URL path for the OpenAPI schema (default ``"/schema.yml"``). + :param static_dir: Directory for static files. Set to ``None`` to disable. Created automatically if missing. + :param static_route: URL prefix for serving static files (default ``"/static"``). + :param templates_dir: Directory for Jinja2 templates (default ``"templates"``). + :param auto_escape: If ``True``, auto-escape HTML/XML in templates. + :param secret_key: Secret key for signing cookie-based sessions. **Always set this in production.** + :param enable_hsts: If ``True``, redirect all HTTP requests to HTTPS. + :param docs_route: URL path for interactive API docs (e.g. ``"/docs"``). Enables OpenAPI if not already set. + :param cors: If ``True``, enable CORS middleware. + :param cors_params: Dict of CORS configuration (``allow_origins``, ``allow_methods``, etc.). + :param allowed_hosts: List of allowed hostnames (e.g. ``["example.com"]``). Defaults to ``["*"]``. + :param openapi_theme: Documentation UI theme: ``"swagger_ui"``, ``"redoc"``, ``"rapidoc"``, or ``"elements"``. + :param lifespan: An async context manager for startup/shutdown logic. + :param request_id: If ``True``, add ``X-Request-ID`` headers to all responses. + """ # noqa: E501 self.background = BackgroundQueue() self.secret_key = secret_key @@ -192,17 +217,38 @@ @property def static_app(self): + """The Starlette ``StaticFiles`` application for serving static assets.""" if not hasattr(self, "_static_app"): assert self.static_dir is not None self._static_app = StaticFiles(directory=self.static_dir) return self._static_app +
+[docs] def before_request(self, websocket=False): + """Register a function to run before every request. + + If the hook sets ``resp.status_code``, the route handler is skipped + and the response is sent immediately (short-circuiting). + + :param websocket: If ``True``, register as a WebSocket before-request hook instead of HTTP. + + Usage:: + + @api.before_request() + def check_auth(req, resp): + if "Authorization" not in req.headers: + resp.status_code = 401 + resp.media = {"error": "unauthorized"} + + """ # noqa: E501 + def decorator(f): self.router.before_request(f, websocket=websocket) return f - return decorator + return decorator
+
[docs] @@ -224,8 +270,26 @@ return decorator
+
+[docs] def add_middleware(self, middleware_cls, **middleware_config): - self.app = middleware_cls(self.app, **middleware_config) + """Add ASGI middleware to the application. + + Middleware wraps the entire application and can inspect or modify + every request and response. Middleware is applied in reverse order — + the last middleware added runs first. + + :param middleware_cls: A Starlette-compatible middleware class. + :param middleware_config: Keyword arguments passed to the middleware constructor. + + Usage:: + + from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware + api.add_middleware(HTTPSRedirectMiddleware) + + """ + self.app = middleware_cls(self.app, **middleware_config)
+
[docs] @@ -590,10 +654,17 @@ uvicorn.run(self, host=address, port=port, **options)
+
+[docs] def run(self, **kwargs): + """Run the application. Shorthand for :meth:`serve` that inherits the ``debug`` setting. + + :param kwargs: Keyword arguments passed through to :meth:`serve`. + """ if "debug" not in kwargs: kwargs.update({"debug": self.debug}) - self.serve(**kwargs) + self.serve(**kwargs)
+
[docs] @@ -621,6 +692,8 @@ +
+[docs] class RouteGroup: """A group of routes with a shared URL prefix.""" @@ -633,7 +706,8 @@ return self.api.route(full_route, **options) def before_request(self, **kwargs): - return self.api.before_request(**kwargs) + return self.api.before_request(**kwargs)
+
diff --git a/_modules/responder/background.html b/_modules/responder/background.html new file mode 100644 index 0000000..bfd8fea --- /dev/null +++ b/_modules/responder/background.html @@ -0,0 +1,175 @@ + + + + + + + responder.background — responder 3.4.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for responder.background

+import asyncio
+import concurrent.futures
+import multiprocessing
+import traceback
+
+from starlette.concurrency import run_in_threadpool
+
+__all__ = ["BackgroundQueue"]
+
+
+
+[docs] +class BackgroundQueue: + """A queue for running tasks in background threads. + + Uses a ``ThreadPoolExecutor`` sized to the number of CPUs. Access it + via ``api.background``. + + Usage:: + + # As a decorator — fire and forget + @api.background.task + def send_email(to, subject): + ... + + send_email("user@example.com", "Hello") + + # Direct submission + future = api.background.run(send_email, "user@example.com", "Hello") + + # As a callable (supports async functions) + await api.background(send_email, "user@example.com", "Hello") + + """ + + def __init__(self, n=None): + """Create a new background queue. + + :param n: Number of worker threads. Defaults to CPU count. + """ + if n is None: + n = multiprocessing.cpu_count() + + self.n = n + self.pool = concurrent.futures.ThreadPoolExecutor(max_workers=n) + self.results = [] + +
+[docs] + def run(self, f, *args, **kwargs): + """Submit a function to run in a background thread. + + :param f: The function to run. + :returns: A ``concurrent.futures.Future`` for the result. + """ + f = self.pool.submit(f, *args, **kwargs) + self.results.append(f) + return f
+ + +
+[docs] + def task(self, f): + """Decorator that wraps a function to run in the background thread pool. + + The decorated function returns a ``Future`` instead of blocking. + Exceptions are printed to stderr via traceback. + + :param f: The function to wrap. + """ + + def on_future_done(fs): + try: + fs.result() + except Exception: + traceback.print_exc() + + def do_task(*args, **kwargs): + result = self.run(f, *args, **kwargs) + result.add_done_callback(on_future_done) + return result + + return do_task
+ + + async def __call__(self, func, *args, **kwargs) -> None: + if asyncio.iscoroutinefunction(func): + return await asyncio.create_task(func(*args, **kwargs)) + return await run_in_threadpool(func, *args, **kwargs)
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/responder/ext/ratelimit.html b/_modules/responder/ext/ratelimit.html new file mode 100644 index 0000000..2dbf046 --- /dev/null +++ b/_modules/responder/ext/ratelimit.html @@ -0,0 +1,161 @@ + + + + + + + responder.ext.ratelimit — responder 3.4.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for responder.ext.ratelimit

+"""Simple in-memory rate limiter for Responder."""
+
+import time
+from collections import defaultdict
+
+
+
+[docs] +class RateLimiter: + """Token bucket rate limiter. + + Usage:: + + from responder.ext.ratelimit import RateLimiter + + limiter = RateLimiter(requests=100, period=60) # 100 req/min + + @api.route(before_request=True) + def rate_limit(req, resp): + limiter.check(req, resp) + + Or use the shorthand:: + + limiter = RateLimiter(requests=100, period=60) + limiter.install(api) + + """ + + def __init__(self, requests=100, period=60): + self.max_requests = requests + self.period = period + self._buckets: dict[str, list[float]] = defaultdict(list) + + def _client_key(self, req): + client = req.client + if client: + return client[0] + return req.headers.get("X-Forwarded-For", "unknown") + + def _cleanup(self, key): + now = time.time() + cutoff = now - self.period + self._buckets[key] = [ + t for t in self._buckets[key] if t > cutoff + ] + +
+[docs] + def check(self, req, resp): + """Check rate limit. Sets 429 status if exceeded.""" + key = self._client_key(req) + self._cleanup(key) + + if len(self._buckets[key]) >= self.max_requests: + resp.status_code = 429 + resp.media = {"error": "rate limit exceeded"} + resp.headers["Retry-After"] = str(self.period) + return False + + self._buckets[key].append(time.time()) + remaining = self.max_requests - len(self._buckets[key]) + resp.headers["X-RateLimit-Limit"] = str(self.max_requests) + resp.headers["X-RateLimit-Remaining"] = str(remaining) + return True
+ + +
+[docs] + def install(self, api): + """Install as a before_request hook on the API.""" + + @api.route(before_request=True) + def _rate_limit(req, resp): + self.check(req, resp)
+
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/responder/models.html b/_modules/responder/models.html index 8931cfd..beec017 100644 --- a/_modules/responder/models.html +++ b/_modules/responder/models.html @@ -4,13 +4,13 @@ - responder.models — responder 3.2.0 documentation + responder.models — responder 3.4.1 documentation - + @@ -88,7 +88,15 @@ self[key] = value +
+[docs] class QueryDict(dict): + """A dictionary for query string parameters that handles multi-value keys. + + Single-value access returns the last value for a key. Use :meth:`get_list` + to retrieve all values for a multi-value parameter. + """ + def __init__(self, query_string): self.update(parse_qs(query_string)) @@ -103,6 +111,8 @@ except IndexError: return [] +
+[docs] def get(self, key, default=None): """ Return the last data value for the passed key. If key doesn't exist @@ -114,7 +124,8 @@ return default if val == []: return default - return val + return val
+ def _get_list(self, key, default=None, force_list=False): """ @@ -134,31 +145,48 @@ values = list(values) if values is not None else None return values +
+[docs] def get_list(self, key, default=None): """ Return the list of values for the key. If key doesn't exist, return a default value. """ - return self._get_list(key, default, force_list=True) + return self._get_list(key, default, force_list=True)
+ +
+[docs] def items(self): """ Yield (key, value) pairs, where value is the last item in the list associated with the key. """ for key in self: - yield key, self[key] + yield key, self[key]
+ +
+[docs] def items_list(self): """ Yield (key, value) pairs, where value is the the list. """ - yield from super().items() + yield from super().items()
+
+
[docs] class Request: + """An HTTP request, passed to each view as the first argument. + + Provides access to headers, cookies, query parameters, the request body, + session data, and more. Most properties are synchronous; reading the body + (via :attr:`content`, :attr:`text`, or :meth:`media`) requires ``await``. + """ + __slots__ = [ "_starlette", "formats", @@ -195,6 +223,7 @@ @property def mimetype(self): + """The MIME type of the request body, from the ``Content-Type`` header.""" return self.headers.get("Content-Type", "") @property @@ -312,6 +341,7 @@ @property def is_secure(self): + """``True`` if the request was made over HTTPS.""" return self.url.scheme == "https"
@@ -366,6 +396,22 @@
[docs] class Response: + """An HTTP response, passed to each view as the second argument. + + Mutate this object to control what gets sent back to the client. Set + :attr:`text`, :attr:`html`, :attr:`media`, or :attr:`content` to define + the body. Use :attr:`headers` and :meth:`set_cookie` to control metadata. + + :var text: Set the response body as plain text (sets ``Content-Type: text/plain``). + :var html: Set the response body as HTML (sets ``Content-Type: text/html``). + :var media: Set a Python object (dict, list) to be serialized as JSON (or negotiated format). + :var content: Set the raw response body as bytes. + :var status_code: The HTTP status code (e.g. ``200``, ``404``). Defaults to ``200`` if not set. + :var headers: A dict of response headers. + :var cookies: A ``SimpleCookie`` holding cookies to set on the response. + :var session: A dict of session data. Changes are persisted in a signed cookie. + """ # noqa: E501 + __slots__ = [ "req", "status_code", @@ -385,28 +431,42 @@ def __init__(self, req, *, formats): self.req = req - #: The HTTP Status Code to use for the Response. self.status_code: int | None = None - self.content = None #: A bytes representation of the response body. + self.content = None self.mimetype = None self.encoding = DEFAULT_ENCODING - self.media = None #: A Python object that will be content-negotiated and - #: sent back to the client. Typically, in JSON formatting. + self.media = None self._stream = None - self.headers = {} #: A Python dictionary of ``{key: value}``, - #: representing the headers of the response. + self.headers = {} self.formats = formats - self.cookies: SimpleCookie = SimpleCookie() #: The cookies set in the Response - self.session = ( - req.session - ) #: The cookie-based session data, in dict form, to add to the Response. + self.cookies: SimpleCookie = SimpleCookie() + self.session = req.session +
+[docs] def stream(self, func, *args, **kwargs): + """Set up a streaming response from an async generator function. + + The generator yields chunks of bytes that are sent to the client + as they are produced, without buffering the full response in memory. + + Usage:: + + @api.route("/stream") + async def stream_data(req, resp): + @resp.stream + async def body(): + for i in range(10): + yield f"chunk {i}\\n".encode() + + :param func: An async generator function that yields response chunks. + """ assert inspect.isasyncgenfunction(func) self._stream = functools.partial(func, *args, **kwargs) - return func + return func
+
[docs] @@ -510,11 +570,20 @@ self.mimetype = guessed or "application/octet-stream"
+
+[docs] def redirect(self, location, *, set_text=True, status_code=HTTP_301): + """Redirect the client to a different URL. + + :param location: The URL to redirect to. + :param set_text: If ``True``, set a default redirect message as the body. + :param status_code: The HTTP status code (default ``301``). + """ self.status_code = status_code if set_text: self.text = f"Redirecting to: {location}" - self.headers.update({"Location": location}) + self.headers.update({"Location": location})
+ @property async def body(self): @@ -545,6 +614,8 @@ {"Content-Type": "application/json"}, ) + + def _prepare_cookies(self, starlette_response): cookie_header = ( @@ -594,10 +685,12 @@ @property def ok(self): + """``True`` if the status code is in the 2xx range (success).""" return 200 <= self.status_code_safe < 300 @property def status_code_safe(self) -> int: + """Return the status code, raising ``RuntimeError`` if it hasn't been set.""" if self.status_code is None: raise RuntimeError("HTTP status code has not been defined") return self.status_code
diff --git a/_modules/responder/status_codes.html b/_modules/responder/status_codes.html index 154d5ce..1bd686e 100644 --- a/_modules/responder/status_codes.html +++ b/_modules/responder/status_codes.html @@ -4,13 +4,13 @@ - responder.status_codes — responder 3.2.0 documentation + responder.status_codes — responder 3.4.1 documentation - + diff --git a/_sources/api.rst.txt b/_sources/api.rst.txt index fc7851f..7bc0506 100644 --- a/_sources/api.rst.txt +++ b/_sources/api.rst.txt @@ -43,6 +43,45 @@ status code, headers, and cookies. :inherited-members: +Route Groups +------------ + +Group related routes under a shared URL prefix — useful for API versioning +and organizing large applications. + +.. autoclass:: responder.api.RouteGroup + :members: + + +Background Queue +---------------- + +Run tasks in background threads without blocking the response. Available +as ``api.background``. + +.. autoclass:: responder.background.BackgroundQueue + :members: + + +Query Dict +---------- + +A dictionary subclass for query string parameters with multi-value support. + +.. autoclass:: responder.models.QueryDict + :members: + + +Rate Limiter +------------ + +In-memory token bucket rate limiter. Limits requests per client IP address +and returns ``429 Too Many Requests`` when exceeded. + +.. autoclass:: responder.ext.ratelimit.RateLimiter + :members: + + Status Code Helpers ------------------- diff --git a/_sources/changes.md.txt b/_sources/changes.md.txt index 3cf2d0b..c485090 100644 --- a/_sources/changes.md.txt +++ b/_sources/changes.md.txt @@ -7,6 +7,44 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +## [v3.2.0] - 2026-03-22 + +### Added + +- Pydantic auto-validation: `request_model` validates input, returns 422 on failure +- Pydantic auto-serialization: `response_model` strips extra fields from responses +- Server-Sent Events: `@resp.sse` for real-time streaming +- `resp.stream_file()` for streaming large files without loading into memory +- `@api.after_request()` hooks +- `api.group("/prefix")` for route groups and API versioning +- `api.graphql("/path", schema=schema)` one-liner GraphQL setup +- `api = responder.API(request_id=True)` for automatic request ID generation +- Built-in rate limiter: `RateLimiter(requests=100, period=60).install(api)` +- MessagePack format support: `await req.media("msgpack")` +- `req.is_json`, `req.path_params`, `req.client` properties +- `api.exception_handler()` decorator for custom error handling +- Lifespan context manager support +- `uuid` and `path` route convertors +- PEP 561 `py.typed` marker +- Pydantic support for OpenAPI schema generation + +### Changed + +- Dependencies flattened: `pip install responder` gets everything +- Core deps reduced to starlette + uvicorn +- TestClient lazy-loaded (no httpx import in production) +- Before-request hooks can short-circuit by setting status code +- Removed poethepoet task runner + +### Fixed + +- Multipart parser losing headers when parts have multiple headers +- `url_for()` with typed route params (`{id:int}`) +- `resp.body` encoding crash on bytes content +- GraphQL text query missing `await` +- Streaming responses not sending Content-Type headers +- Python 3.9 compatibility for union type syntax + ## [v3.0.0] - 2026-03-22 ### Added diff --git a/_sources/guide-config.rst.txt b/_sources/guide-config.rst.txt new file mode 100644 index 0000000..ce62f6e --- /dev/null +++ b/_sources/guide-config.rst.txt @@ -0,0 +1,172 @@ +Configuration +============= + +Every application needs different settings for different environments — +debug mode in development, real secrets in production, different database +URLs for testing. This guide covers how to manage configuration cleanly. + + +Environment Variables +--------------------- + +The simplest and most universal approach. Environment variables work +everywhere — locally, in Docker, on cloud platforms — and keep secrets +out of your source code:: + + import os + import responder + + api = responder.API( + debug=os.getenv("DEBUG", "false").lower() == "true", + secret_key=os.environ["SECRET_KEY"], + cors=os.getenv("CORS_ENABLED", "false").lower() == "true", + ) + +Some variables Responder handles automatically: + +- ``PORT`` — when set, the server binds to ``0.0.0.0`` on this port + +Set variables in your shell:: + + $ export SECRET_KEY="your-secret-here" + $ export DEBUG=true + $ python app.py + +Or in a ``.env`` file (don't commit this to git):: + + SECRET_KEY=your-secret-here + DEBUG=true + + +Using .env Files +---------------- + +For local development, a ``.env`` file is convenient. Install +``python-dotenv`` and load it at the top of your app:: + + $ uv pip install python-dotenv + +:: + + from dotenv import load_dotenv + load_dotenv() + + import os + import responder + + api = responder.API( + secret_key=os.environ["SECRET_KEY"], + ) + +Add ``.env`` to your ``.gitignore`` — never commit secrets. + + +Configuration Class Pattern +---------------------------- + +For larger applications, a configuration class keeps things organized:: + + import os + + class Config: + SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret") + DEBUG = os.environ.get("DEBUG", "false").lower() == "true" + DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///dev.db") + CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "").split(",") + + config = Config() + + api = responder.API( + debug=config.DEBUG, + secret_key=config.SECRET_KEY, + cors=bool(config.CORS_ORIGINS[0]), + cors_params={"allow_origins": config.CORS_ORIGINS}, + ) + +This makes it easy to see all your settings in one place. + + +Secret Key +---------- + +The ``secret_key`` is used to sign session cookies. If someone knows your +secret key, they can forge session data and impersonate any user. + +Rules: + +- **Never use the default** in production +- **Generate a random key**: ``python -c "import secrets; print(secrets.token_hex(32))"`` +- **Store it in an environment variable**, not in code +- **Rotate it** if it's ever compromised (this invalidates all sessions) + +:: + + api = responder.API(secret_key=os.environ["SECRET_KEY"]) + + +Debug Mode +---------- + +Debug mode controls error page behavior: + +- **On** (``debug=True``): detailed error pages with tracebacks. Never + use this in production — it exposes your source code. +- **Off** (``debug=False``): generic error pages. This is the default. + +:: + + api = responder.API(debug=True) # development only + +A common pattern is to read it from the environment:: + + api = responder.API(debug=os.getenv("DEBUG") == "true") + + +Allowed Hosts +------------- + +In production, always set ``allowed_hosts`` to prevent Host header +attacks. This should match the domain names your application serves:: + + api = responder.API( + allowed_hosts=["example.com", "www.example.com"], + ) + +In development, you can use ``["*"]`` (the default) or specific local +addresses:: + + api = responder.API(allowed_hosts=["localhost", "127.0.0.1"]) + + +Putting It All Together +----------------------- + +A production-ready configuration setup:: + + import os + from dotenv import load_dotenv + + load_dotenv() + + import responder + + api = responder.API( + debug=os.getenv("DEBUG", "false") == "true", + secret_key=os.environ["SECRET_KEY"], + allowed_hosts=os.getenv("ALLOWED_HOSTS", "*").split(","), + cors=bool(os.getenv("CORS_ORIGINS")), + cors_params={ + "allow_origins": os.getenv("CORS_ORIGINS", "").split(","), + "allow_methods": ["GET", "POST", "PUT", "DELETE"], + }, + ) + +With a ``.env`` file for local development:: + + SECRET_KEY=dev-secret-do-not-use-in-prod + DEBUG=true + ALLOWED_HOSTS=localhost,127.0.0.1 + CORS_ORIGINS=http://localhost:3000 + +And environment variables set properly in production (via your cloud +platform's dashboard, Docker secrets, or a secrets manager). diff --git a/_sources/index.rst.txt b/_sources/index.rst.txt index 7455866..6b9ba39 100644 --- a/_sources/index.rst.txt +++ b/_sources/index.rst.txt @@ -86,7 +86,7 @@ Installation $ uv pip install responder -Python 3.9 and above. That's it. +Python 3.10 and above. That's it. .. toctree:: @@ -106,7 +106,11 @@ Python 3.9 and above. That's it. tutorial-rest tutorial-sqlalchemy + tutorial-auth + tutorial-websockets + tutorial-middleware tutorial-flask + guide-config .. toctree:: :maxdepth: 1 diff --git a/_sources/quickstart.rst.txt b/_sources/quickstart.rst.txt index 1781eed..a36cdc7 100644 --- a/_sources/quickstart.rst.txt +++ b/_sources/quickstart.rst.txt @@ -330,3 +330,49 @@ for lightweight use cases where you don't need a full message broker. 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. + + +Putting It All Together +----------------------- + +Here's a complete, working Responder application that combines everything +from this guide:: + + import responder + + api = responder.API() + + @api.route("/") + def index(req, resp): + resp.text = "Welcome to the API" + + @api.route("/hello/{name}") + def greet(req, resp, *, name): + resp.media = {"message": f"hello, {name}!"} + + @api.route("/add/{a:int}/{b:int}") + def add(req, resp, *, a, b): + resp.media = {"result": a + b} + + @api.route("/echo", methods=["POST"]) + async def echo(req, resp): + data = await req.media() + resp.media = {"received": data} + + if __name__ == "__main__": + api.run() + +Save this as ``app.py``, run it with ``python app.py``, and try:: + + $ curl http://localhost:5042/ + $ curl http://localhost:5042/hello/world + $ curl http://localhost:5042/add/3/4 + $ curl -X POST http://localhost:5042/echo \ + -H "Content-Type: application/json" -d '{"key": "value"}' + +From here, explore the :doc:`tour` for the full range of features, or +jump into the tutorials: + +- :doc:`tutorial-rest` — build a full CRUD API with validation +- :doc:`tutorial-sqlalchemy` — connect to a database +- :doc:`tutorial-auth` — add authentication diff --git a/_sources/tour.rst.txt b/_sources/tour.rst.txt index 962c720..f041e3f 100644 --- a/_sources/tour.rst.txt +++ b/_sources/tour.rst.txt @@ -416,6 +416,22 @@ Requests to ``/flask/`` will be handled by Flask. Everything else goes through Responder. Both WSGI and ASGI apps are supported — Responder wraps WSGI apps in an ASGI adapter automatically. +You can also mount `marimo `_ notebooks as +interactive dashboards within your API:: + + import marimo + + server = ( + marimo.create_asgi_app() + .with_app(path="", root="./notebooks/dashboard.py") + .with_app(path="/analysis", root="./notebooks/analysis.py") + ) + + api.mount("/notebooks", server.build()) + +Notebooks are served at ``/notebooks/`` and ``/notebooks/analysis``, +with full interactivity — reactive cells, widgets, plots, and all. + Cookies ------- diff --git a/_sources/tutorial-auth.rst.txt b/_sources/tutorial-auth.rst.txt new file mode 100644 index 0000000..5e40408 --- /dev/null +++ b/_sources/tutorial-auth.rst.txt @@ -0,0 +1,191 @@ +Authentication +============== + +Every API that handles user data needs authentication — a way to verify +who is making a request. This guide covers the most common patterns: +API keys, JWT tokens, and how to build reusable auth guards with +Responder's before-request hooks. + + +API Key Authentication +---------------------- + +The simplest approach. The client sends a secret key in a header, and +your server checks it against a known value. This is common for +server-to-server communication and simple APIs:: + + API_KEYS = {"sk-abc123", "sk-def456"} + + @api.route(before_request=True) + def check_api_key(req, resp): + key = req.headers.get("X-API-Key") + if key not in API_KEYS: + resp.status_code = 401 + resp.media = {"error": "Invalid or missing API key"} + +Because the before-request hook sets ``resp.status_code``, the route +handler is skipped entirely for unauthorized requests. The client never +reaches your endpoint — the guard catches them first. + +The client sends the key like this:: + + $ curl -H "X-API-Key: sk-abc123" http://localhost:5042/protected + + +Bearer Token Authentication +---------------------------- + +Bearer tokens are the standard for modern APIs. The client sends a token +in the ``Authorization`` header, and the server validates it. The most +common format is `JWT `_ (JSON Web Tokens). + +Install PyJWT:: + + $ uv pip install pyjwt + +Create a helper to encode and decode tokens:: + + import jwt + from datetime import datetime, timedelta + + SECRET = "your-secret-key" + + def create_token(user_id: int) -> str: + payload = { + "sub": user_id, + "exp": datetime.utcnow() + timedelta(hours=24), + } + return jwt.encode(payload, SECRET, algorithm="HS256") + + def verify_token(token: str) -> dict | None: + try: + return jwt.decode(token, SECRET, algorithms=["HS256"]) + except jwt.InvalidTokenError: + return None + +Add a login endpoint that issues tokens, and a before-request hook that +verifies them:: + + @api.route("/login", methods=["POST"]) + async def login(req, resp): + data = await req.media() + # In a real app, check credentials against a database + if data.get("username") == "admin" and data.get("password") == "secret": + token = create_token(user_id=1) + resp.media = {"token": token} + else: + resp.status_code = 401 + resp.media = {"error": "Invalid credentials"} + + @api.route(before_request=True) + def auth_guard(req, resp): + # Skip auth for the login endpoint itself + if req.url.path == "/login": + return + + auth = req.headers.get("Authorization", "") + if not auth.startswith("Bearer "): + resp.status_code = 401 + resp.media = {"error": "Missing bearer token"} + return + + token = auth[7:] # Strip "Bearer " + payload = verify_token(token) + if payload is None: + resp.status_code = 401 + resp.media = {"error": "Invalid or expired token"} + return + + # Store the authenticated user on the request state + req.state.user_id = payload["sub"] + +Now any route can access the authenticated user:: + + @api.route("/me") + def get_me(req, resp): + resp.media = {"user_id": req.state.user_id} + +The client flow: + +1. ``POST /login`` with credentials → receive a token +2. Include ``Authorization: Bearer `` on every subsequent request +3. The token expires after 24 hours — the client must log in again + + +Skipping Auth for Public Routes +-------------------------------- + +The example above skips auth for ``/login`` by checking the path. For +more control, you can use a set of public paths:: + + PUBLIC_PATHS = {"/login", "/signup", "/health", "/docs", "/schema.yml"} + + @api.route(before_request=True) + def auth_guard(req, resp): + if req.url.path in PUBLIC_PATHS: + return + # ... check token + + +Custom Exception for Auth Errors +--------------------------------- + +For cleaner code, define a custom exception and register a handler:: + + class AuthError(Exception): + def __init__(self, message="Unauthorized", status_code=401): + self.message = message + self.status_code = status_code + + @api.exception_handler(AuthError) + async def handle_auth_error(req, resp, exc): + resp.status_code = exc.status_code + resp.media = {"error": exc.message} + +Now your auth guard can simply raise:: + + @api.route(before_request=True) + def auth_guard(req, resp): + if req.url.path in PUBLIC_PATHS: + return + if "Authorization" not in req.headers: + raise AuthError("Missing authorization header") + + +Using Sessions for Web Apps +---------------------------- + +For traditional web applications (with HTML pages and forms), cookie-based +sessions are simpler than tokens. The browser handles cookies automatically +— no client-side token management needed:: + + @api.route("/login", methods=["POST"]) + async def login(req, resp): + data = await req.media("form") + if data["username"] == "admin" and data["password"] == "secret": + resp.session["user"] = data["username"] + api.redirect(resp, location="/dashboard") + else: + resp.status_code = 401 + resp.html = "

Invalid credentials

" + + @api.route("/dashboard") + def dashboard(req, resp): + user = req.session.get("user") + if not user: + api.redirect(resp, location="/login") + return + resp.html = f"

Welcome, {user}!

" + + @api.route("/logout") + def logout(req, resp): + resp.session.clear() + api.redirect(resp, location="/login") + +Remember to set a proper secret key:: + + api = responder.API(secret_key="your-production-secret-key") + +The session data is signed (not encrypted) — users can read it but +can't tamper with it. Don't store sensitive data like passwords in +sessions. diff --git a/_sources/tutorial-middleware.rst.txt b/_sources/tutorial-middleware.rst.txt new file mode 100644 index 0000000..5679d35 --- /dev/null +++ b/_sources/tutorial-middleware.rst.txt @@ -0,0 +1,129 @@ +Writing Middleware +================== + +Middleware sits between the server and your route handlers, processing +every request and response that flows through your application. It's the +right tool for cross-cutting concerns — things that apply to *all* +requests, not just specific routes. + +Common middleware use cases: + +- Request logging and timing +- Authentication and authorization +- Adding security headers +- Request ID generation +- Rate limiting +- Response compression (built-in) + + +Hooks vs. Middleware +-------------------- + +Responder gives you two levels of request processing: + +**Hooks** (``before_request`` / ``after_request``) run inside Responder's +routing layer. They receive Responder's ``req`` and ``resp`` objects and +are the simplest way to add behavior:: + + @api.route(before_request=True) + def add_header(req, resp): + resp.headers["X-Powered-By"] = "Responder" + + @api.after_request() + def log_request(req, resp): + print(f"{req.method} {req.url.path} -> {resp.status_code}") + +**Middleware** runs at the ASGI level, wrapping the entire application. +It's more powerful but more complex — you work with raw ASGI scopes +instead of Responder objects. Use middleware when you need to process +requests *before* they reach Responder's routing, or when you need to +integrate with Starlette middleware. + + +Using Starlette Middleware +-------------------------- + +Responder is built on Starlette, so any Starlette middleware works +out of the box:: + + from starlette.middleware.base import BaseHTTPMiddleware + + class TimingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request, call_next): + import time + start = time.time() + response = await call_next(request) + duration = time.time() - start + response.headers["X-Response-Time"] = f"{duration:.3f}s" + return response + + api.add_middleware(TimingMiddleware) + +The ``dispatch`` method receives a Starlette ``Request`` and a +``call_next`` function. Call ``call_next(request)`` to pass the request +to the next middleware (or to your route handler). The return value is +a Starlette ``Response`` that you can modify before it's sent. + + +Built-in Middleware +------------------- + +Responder configures several middleware components automatically: + +- **GZipMiddleware** — compresses responses larger than 500 bytes +- **TrustedHostMiddleware** — validates the ``Host`` header +- **ServerErrorMiddleware** — catches unhandled exceptions +- **ExceptionMiddleware** — routes exceptions to your handlers +- **SessionMiddleware** — manages signed cookie sessions + +Optional middleware you can enable: + +- **CORSMiddleware** — ``api = responder.API(cors=True)`` +- **HTTPSRedirectMiddleware** — ``api = responder.API(enable_hsts=True)`` + + +Adding Third-Party Middleware +----------------------------- + +Any ASGI middleware can be added with ``api.add_middleware()``:: + + from some_package import SomeMiddleware + + api.add_middleware(SomeMiddleware, option1="value", option2=True) + +Keyword arguments are passed to the middleware's constructor. + + +Middleware Order +---------------- + +Middleware wraps your application like layers of an onion. The *last* +middleware added is the *outermost* layer — it sees the request first +and the response last. + +Responder's built-in middleware stack (from outermost to innermost): + +1. SessionMiddleware +2. ServerErrorMiddleware +3. CORSMiddleware (if enabled) +4. TrustedHostMiddleware +5. HTTPSRedirectMiddleware (if enabled) +6. GZipMiddleware +7. ExceptionMiddleware +8. Your routes + +When you call ``api.add_middleware()``, your middleware is added *outside* +the existing stack. Keep this in mind for ordering dependencies — if +middleware A depends on middleware B having run first, add B before A. + + +When to Use What +----------------- + +- **Simple header additions, logging, auth checks** → use hooks +- **Response transformation, timing, third-party integrations** → use middleware +- **Rate limiting** → use the built-in ``RateLimiter`` (it uses hooks internally) +- **Request ID** → use ``api = responder.API(request_id=True)`` + +Start with hooks. They're simpler and cover most cases. Graduate to +middleware when hooks aren't enough. diff --git a/_sources/tutorial-websockets.rst.txt b/_sources/tutorial-websockets.rst.txt new file mode 100644 index 0000000..62f17ae --- /dev/null +++ b/_sources/tutorial-websockets.rst.txt @@ -0,0 +1,171 @@ +WebSocket Tutorial +================== + +HTTP is request-response — the client asks, the server answers, and the +connection closes. WebSockets upgrade that into a persistent, bidirectional +channel where both sides can send messages at any time. This is what powers +chat apps, live dashboards, multiplayer games, and collaborative editors. + +This tutorial builds a simple chat room to show how WebSockets work in +Responder. + + +How WebSockets Work +------------------- + +1. The client sends a normal HTTP request with an ``Upgrade: websocket`` + header. +2. The server accepts the upgrade and the connection switches protocols. +3. Both sides can now send messages freely — no more request/response. +4. Either side can close the connection at any time. + +In Responder, WebSocket routes receive a ``ws`` object instead of +``req`` and ``resp``. The ``ws`` object has methods for accepting the +connection, sending and receiving data, and closing. + + +Echo Server +----------- + +The simplest WebSocket — echoes everything back:: + + @api.route("/ws", websocket=True) + async def echo(ws): + await ws.accept() + while True: + data = await ws.receive_text() + await ws.send_text(f"Echo: {data}") + +The ``await ws.accept()`` call completes the WebSocket handshake. After +that, you're in a loop — receive a message, send a response. + +Test it with a WebSocket client:: + + $ pip install websocket-client + $ python -c " + import websocket + ws = websocket.create_connection('ws://localhost:5042/ws') + ws.send('hello') + print(ws.recv()) # Echo: hello + ws.close() + " + + +Chat Room +--------- + +A chat room needs to broadcast messages to all connected clients. We keep +a set of active connections and iterate through them when someone sends +a message:: + + connected = set() + + @api.route("/chat", websocket=True) + async def chat(ws): + await ws.accept() + connected.add(ws) + try: + while True: + message = await ws.receive_text() + # Broadcast to all connected clients + for client in connected: + await client.send_text(message) + except Exception: + pass + finally: + connected.discard(ws) + +The ``try/finally`` block ensures we remove disconnected clients from +the set, even if the connection drops unexpectedly. + + +Data Formats +------------ + +WebSockets support three data formats: + +**Text** — plain strings:: + + await ws.send_text("hello") + message = await ws.receive_text() + +**JSON** — auto-serialized Python objects:: + + await ws.send_json({"type": "update", "data": [1, 2, 3]}) + message = await ws.receive_json() + +**Binary** — raw bytes, useful for images, audio, or custom protocols:: + + await ws.send_bytes(b"\x00\x01\x02") + data = await ws.receive_bytes() + + +HTML Client +----------- + +Here's a minimal HTML page that connects to the chat room. The browser's +built-in ``WebSocket`` API handles everything — no libraries needed: + +.. code-block:: html + + + + +
+ + + + + +Save this as ``static/index.html`` and serve it with Responder's +built-in static file support. + + +Before-Request Hooks for WebSockets +------------------------------------ + +You can run code before a WebSocket connection is established, just like +HTTP before-request hooks. This is useful for authentication:: + + @api.before_request(websocket=True) + async def ws_auth(ws): + # Check for a token in the query string + # (WebSocket headers are limited in browsers) + await ws.accept() + +WebSocket before-request hooks receive the ``ws`` object and must call +``await ws.accept()`` if they want the connection to proceed. + + +Testing WebSockets +------------------ + +Use Starlette's ``TestClient`` for WebSocket tests:: + + from starlette.testclient import TestClient + + def test_echo(): + client = TestClient(api) + with client.websocket_connect("/ws") as ws: + ws.send_text("hello") + assert ws.receive_text() == "Echo: hello" + +The ``websocket_connect`` context manager handles the connection +lifecycle — it connects on enter and disconnects on exit. diff --git a/_static/documentation_options.js b/_static/documentation_options.js index b1611d5..3d20959 100644 --- a/_static/documentation_options.js +++ b/_static/documentation_options.js @@ -1,5 +1,5 @@ const DOCUMENTATION_OPTIONS = { - VERSION: '3.2.0', + VERSION: '3.4.1', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/api.html b/api.html index 45e147e..8b23b4d 100644 --- a/api.html +++ b/api.html @@ -5,13 +5,13 @@ - API Reference — responder 3.2.0 documentation + API Reference — responder 3.4.1 documentation - + @@ -78,6 +78,28 @@ module and use it to define your entire web service.

+
+
+add_middleware(middleware_cls, **middleware_config)[source]
+

Add ASGI middleware to the application.

+

Middleware wraps the entire application and can inspect or modify +every request and response. Middleware is applied in reverse order — +the last middleware added runs first.

+
+
Parameters:
+
    +
  • middleware_cls – A Starlette-compatible middleware class.

  • +
  • middleware_config – Keyword arguments passed to the middleware constructor.

  • +
+
+
+

Usage:

+
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
+api.add_middleware(HTTPSRedirectMiddleware)
+
+
+
+
add_route(route=None, endpoint=None, *, default=False, static=True, check_existing=True, websocket=False, before_request=False, methods=None)[source]
@@ -108,6 +130,27 @@ Also, it will become a default route.

+
+
+before_request(websocket=False)[source]
+

Register a function to run before every request.

+

If the hook sets resp.status_code, the route handler is skipped +and the response is sent immediately (short-circuiting).

+
+
Parameters:
+

websocket – If True, register as a WebSocket before-request hook instead of HTTP.

+
+
+

Usage:

+
@api.before_request()
+def check_auth(req, resp):
+    if "Authorization" not in req.headers:
+        resp.status_code = 401
+        resp.media = {"error": "unauthorized"}
+
+
+
+
exception_handler(exception_cls)[source]
@@ -261,6 +304,17 @@ representing the HTTP status code of the redirect.

+
+
+run(**kwargs)[source]
+

Run the application. Shorthand for serve() that inherits the debug setting.

+
+
Parameters:
+

kwargs – Keyword arguments passed through to serve().

+
+
+
+
schema(name, **options)[source]
@@ -304,6 +358,12 @@ able to send HTTP requests to the Responder application.

+
+
+property static_app
+

The Starlette StaticFiles application for serving static assets.

+
+
template(filename, *args, **kwargs)[source]
@@ -361,7 +421,11 @@ because it involves I/O.

class responder.Request(scope, receive, api=None, formats=None)[source]
-
+

An HTTP request, passed to each view as the first argument.

+

Provides access to headers, cookies, query parameters, the request body, +session data, and more. Most properties are synchronous; reading the body +(via content, text, or media()) requires await.

+
accepts(content_type)[source]

Returns True if the incoming Request accepts the given content_type.

@@ -416,6 +480,12 @@ because it involves I/O.

Returns True if the request content type is JSON.

+
+
+property is_secure
+

True if the request was made over HTTPS.

+
+
async media(format: str | Callable = None)[source]
@@ -434,6 +504,12 @@ Alternatively, accepts a custom callable for the format type.

The incoming HTTP method used for the request, lower-cased.

+
+
+property mimetype
+

The MIME type of the request body, from the Content-Type header.

+
+
property params
@@ -485,18 +561,24 @@ status code, headers, and cookies.

class responder.Response(req, *, formats)[source]
-
-
-content
-

A bytes representation of the response body.

-
- -
-
-cookies: SimpleCookie
-

The cookies set in the Response

-
- +

An HTTP response, passed to each view as the second argument.

+

Mutate this object to control what gets sent back to the client. Set +text, html, media, or content to define +the body. Use headers and set_cookie() to control metadata.

+
+
Variables:
+
    +
  • text – Set the response body as plain text (sets Content-Type: text/plain).

  • +
  • html – Set the response body as HTML (sets Content-Type: text/html).

  • +
  • media – Set a Python object (dict, list) to be serialized as JSON (or negotiated format).

  • +
  • content – Set the raw response body as bytes.

  • +
  • status_code – The HTTP status code (e.g. 200, 404). Defaults to 200 if not set.

  • +
  • headers – A dict of response headers.

  • +
  • cookies – A SimpleCookie holding cookies to set on the response.

  • +
  • session – A dict of session data. Changes are persisted in a signed cookie.

  • +
+
+
file(path, *, content_type=None)[source]
@@ -511,28 +593,52 @@ status code, headers, and cookies.

-
-
-formats
-

representing the headers of the response.

+
+
+property ok
+

True if the status code is in the 2xx range (success).

-
-
-headers
-

A Python dictionary of {key: value},

+
+
+redirect(location, *, set_text=True, status_code=301)[source]
+

Redirect the client to a different URL.

+
+
Parameters:
+
    +
  • location – The URL to redirect to.

  • +
  • set_text – If True, set a default redirect message as the body.

  • +
  • status_code – The HTTP status code (default 301).

  • +
+
+
-
-
-media
-

A Python object that will be content-negotiated and

-
- -
-
-session
-

The cookie-based session data, in dict form, to add to the Response.

+
+ +

Set a cookie on the response with full control over directives.

+
+
Parameters:
+
    +
  • key – The cookie name.

  • +
  • value – The cookie value.

  • +
  • expires – Expiration date string (e.g. "Thu, 01 Jan 2026 00:00:00 GMT").

  • +
  • path – URL path the cookie applies to (default "/").

  • +
  • domain – Domain the cookie is valid for.

  • +
  • max_age – Maximum age in seconds before the cookie expires.

  • +
  • secure – If True, cookie is only sent over HTTPS.

  • +
  • httponly – If True (default), cookie is inaccessible to JavaScript.

  • +
+
+
+

Usage:

+
resp.set_cookie(
+    "token", value="abc123",
+    max_age=3600, secure=True, httponly=True,
+)
+
+
@@ -552,10 +658,32 @@ status code, headers, and cookies.

Yielding a string is treated as data.

-
-
-status_code: int | None
-

The HTTP Status Code to use for the Response.

+
+
+property status_code_safe: int
+

Return the status code, raising RuntimeError if it hasn’t been set.

+
+ +
+
+stream(func, *args, **kwargs)[source]
+

Set up a streaming response from an async generator function.

+

The generator yields chunks of bytes that are sent to the client +as they are produced, without buffering the full response in memory.

+

Usage:

+
@api.route("/stream")
+async def stream_data(req, resp):
+    @resp.stream
+    async def body():
+        for i in range(10):
+            yield f"chunk {i}\n".encode()
+
+
+
+
Parameters:
+

func – An async generator function that yields response chunks.

+
+
@@ -575,6 +703,149 @@ Yielding a string is treated as data.

+ +
+

Route Groups

+

Group related routes under a shared URL prefix — useful for API versioning +and organizing large applications.

+
+
+class responder.api.RouteGroup(api, prefix)[source]
+

A group of routes with a shared URL prefix.

+
+ +
+
+

Background Queue

+

Run tasks in background threads without blocking the response. Available +as api.background.

+
+
+class responder.background.BackgroundQueue(n=None)[source]
+

A queue for running tasks in background threads.

+

Uses a ThreadPoolExecutor sized to the number of CPUs. Access it +via api.background.

+

Usage:

+
# As a decorator — fire and forget
+@api.background.task
+def send_email(to, subject):
+    ...
+
+send_email("user@example.com", "Hello")
+
+# Direct submission
+future = api.background.run(send_email, "user@example.com", "Hello")
+
+# As a callable (supports async functions)
+await api.background(send_email, "user@example.com", "Hello")
+
+
+
+
+run(f, *args, **kwargs)[source]
+

Submit a function to run in a background thread.

+
+
Parameters:
+

f – The function to run.

+
+
Returns:
+

A concurrent.futures.Future for the result.

+
+
+
+ +
+
+task(f)[source]
+

Decorator that wraps a function to run in the background thread pool.

+

The decorated function returns a Future instead of blocking. +Exceptions are printed to stderr via traceback.

+
+
Parameters:
+

f – The function to wrap.

+
+
+
+ +
+ +
+
+

Query Dict

+

A dictionary subclass for query string parameters with multi-value support.

+
+
+class responder.models.QueryDict(query_string)[source]
+

A dictionary for query string parameters that handles multi-value keys.

+

Single-value access returns the last value for a key. Use get_list() +to retrieve all values for a multi-value parameter.

+
+
+get(key, default=None)[source]
+

Return the last data value for the passed key. If key doesn’t exist +or value is an empty list, return default.

+
+ +
+
+get_list(key, default=None)[source]
+

Return the list of values for the key. If key doesn’t exist, return a +default value.

+
+ +
+
+items()[source]
+

Yield (key, value) pairs, where value is the last item in the list +associated with the key.

+
+ +
+
+items_list()[source]
+

Yield (key, value) pairs, where value is the the list.

+
+ +
+ +
+
+

Rate Limiter

+

In-memory token bucket rate limiter. Limits requests per client IP address +and returns 429 Too Many Requests when exceeded.

+
+
+class responder.ext.ratelimit.RateLimiter(requests=100, period=60)[source]
+

Token bucket rate limiter.

+

Usage:

+
from responder.ext.ratelimit import RateLimiter
+
+limiter = RateLimiter(requests=100, period=60)  # 100 req/min
+
+@api.route(before_request=True)
+def rate_limit(req, resp):
+    limiter.check(req, resp)
+
+
+

Or use the shorthand:

+
limiter = RateLimiter(requests=100, period=60)
+limiter.install(api)
+
+
+
+
+check(req, resp)[source]
+

Check rate limit. Sets 429 status if exceeded.

+
+ +
+
+install(api)[source]
+

Install as a before_request hook on the API.

+
+ +
+

Status Code Helpers

@@ -635,8 +906,10 @@ into. Useful in middleware and after-request hooks.

  • The API Class
  • -
    -

    v3.0.0 - 2026-03-22

    +
    +

    [v3.2.0] - 2026-03-22

    Added

      +
    • Pydantic auto-validation: request_model validates input, returns 422 on failure

    • +
    • Pydantic auto-serialization: response_model strips extra fields from responses

    • +
    • Server-Sent Events: @resp.sse for real-time streaming

    • +
    • resp.stream_file() for streaming large files without loading into memory

    • +
    • @api.after_request() hooks

    • +
    • api.group("/prefix") for route groups and API versioning

    • +
    • api.graphql("/path", schema=schema) one-liner GraphQL setup

    • +
    • api = responder.API(request_id=True) for automatic request ID generation

    • +
    • Built-in rate limiter: RateLimiter(requests=100, period=60).install(api)

    • +
    • MessagePack format support: await req.media("msgpack")

    • +
    • req.is_json, req.path_params, req.client properties

    • +
    • api.exception_handler() decorator for custom error handling

    • +
    • Lifespan context manager support

    • +
    • uuid and path route convertors

    • +
    • PEP 561 py.typed marker

    • +
    • Pydantic support for OpenAPI schema generation

    • +
    +
    +
    +

    Changed

    +
      +
    • Dependencies flattened: pip install responder gets everything

    • +
    • Core deps reduced to starlette + uvicorn

    • +
    • TestClient lazy-loaded (no httpx import in production)

    • +
    • Before-request hooks can short-circuit by setting status code

    • +
    • Removed poethepoet task runner

    • +
    +
    +
    +

    Fixed

    +
      +
    • Multipart parser losing headers when parts have multiple headers

    • +
    • url_for() with typed route params ({id:int})

    • +
    • resp.body encoding crash on bytes content

    • +
    • GraphQL text query missing await

    • +
    • Streaming responses not sending Content-Type headers

    • +
    • Python 3.9 compatibility for union type syntax

    • +
    +
    +
    +
    +

    v3.0.0 - 2026-03-22

    +
    +

    Added

    +
    • Platform: Added support for Python 3.10 - Python 3.13

    • CLI: responder run now also accepts a filesystem path on its <target> argument, enabling usage on single-file applications.

    • CLI: responder run now also accepts URLs.

    -
    -

    Changed

    +
    +

    Changed

    • Platform: Minimum Python version is now 3.9 (dropped 3.6, 3.7, 3.8)

    • Dependencies: Dramatically reduced core dependency count (10 → 5)

      @@ -91,8 +136,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    • CLI: responder run --build ceased to exist

    -
    -

    Fixed

    +
    +

    Fixed

    • Routing: Fixed dispatching static_route=None on Windows

    • uvicorn: --debug now maps to uvicorn’s log_level = "debug"

    • @@ -102,8 +147,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v2.0.5 - 2019-12-15

    -
    -

    Added

    +
    +

    Added

    • Update requirements to support python 3.8

    @@ -111,8 +156,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v2.0.4 - 2019-11-19

    -
    -

    Fixed

    +
    +

    Fixed

    • Fix static app resolving

    @@ -120,8 +165,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v2.0.3 - 2019-09-20

    -
    -

    Fixed

    +
    +

    Fixed

    • Fix template conflicts

    @@ -129,8 +174,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v2.0.2 - 2019-09-20

    -
    -

    Fixed

    +
    +

    Fixed

    • Fix template conflicts

    @@ -138,8 +183,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v2.0.1 - 2019-09-20

    -
    -

    Fixed

    +
    +

    Fixed

    • Fix template import

    @@ -147,8 +192,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v2.0.0 - 2019-09-19

    -
    -

    Changed

    +
    +

    Changed

    • Refactor Router and Schema

    @@ -156,8 +201,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.3.2 - 2019-08-15

    -
    -

    Added

    +
    +

    Added

    • ASGI 3 support

    • CI tests for python 3.8-dev

    • @@ -173,15 +218,15 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.3.1 - 2019-04-28

    -
    -

    Added

    +
    +

    Added

    • Route params Converters

    • Add search for documentation pages

    -
    -

    Changed

    +
    +

    Changed

    • Bump dependencies

    @@ -189,8 +234,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.3.0 - 2019-02-22

    -
    -

    Fixed

    +
    +

    Fixed

    • Versioning issue

    • Multiple cookies.

    • @@ -198,8 +243,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    • Other bugfixes.

    -
    -

    Added

    +
    +

    Added

    • Stream support via resp.stream.

    • Cookie directives via resp.set_cookie.

    • @@ -210,14 +255,14 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.1.3 - 2019-01-12

    -
    -

    Changed

    +
    +

    Changed

    • Refactor _route_for

    -
    -

    Fixed

    +
    +

    Fixed

    • Resolve startup/shutdwown events

    @@ -225,21 +270,21 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.2.0 - 2018-12-29

    -
    -

    Added

    +
    +

    Added

    • Documentations

    -
    -

    Changed

    +
    +

    Changed

    • Use Starlette’s LifeSpan middleware

    • Update denpendencies

    -
    -

    Fixed

    +
    +

    Fixed

    • Fix route.is_class_based

    • Fix test_500

    • @@ -249,8 +294,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.1.2 - 2018-11-11

    -
    -

    Fixed

    +
    +

    Fixed

    • Minor fixes for Open API

    • Typos

    • @@ -259,8 +304,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.1.1 - 2018-10-29

    -
    -

    Changed

    +
    +

    Changed

    • Run sync views in a threadpoolexecutor.

    @@ -268,8 +313,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.1.0 - 2018-10-27

    -
    -

    Added

    +
    +

    Added

    • Support for before_request.

    @@ -277,8 +322,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.0.5- 2018-10-27

    -
    -

    Fixed

    +
    +

    Fixed

    • Fix sessions.

    @@ -286,8 +331,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.0.4 - 2018-10-27

    -
    -

    Fixed

    +
    +

    Fixed

    • Potential bufix for cookies.

    @@ -295,8 +340,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.0.3 - 2018-10-27

    -
    -

    Fixed

    +
    +

    Fixed

    • Bugfix for redirects.

    @@ -304,8 +349,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.0.2 - 2018-10-27

    -
    -

    Changed

    +
    +

    Changed

    • Improvement for static file hosting.

    @@ -313,8 +358,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.0.1 - 2018-10-26

    -
    -

    Changed

    +
    +

    Changed

    • Improve cors configuration settings.

    @@ -322,8 +367,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v1.0.0 - 2018-10-26

    -
    -

    Changed

    +
    +

    Changed

    • Move GraphQL support into a built-in plugin.

    @@ -331,14 +376,14 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.3.3 - 2018-10-25

    -
    -

    Added

    +
    +

    Added

    • CORS support

    -
    -

    Changed

    +
    +

    Changed

    • Improved exceptions.

    @@ -346,8 +391,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.3.2 - 2018-10-25

    -
    -

    Changed

    +
    +

    Changed

    • Subtle improvements.

    @@ -355,8 +400,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.3.1 - 2018-10-24

    -
    -

    Fixed

    +
    +

    Fixed

    • Packaging fix.

    @@ -364,8 +409,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.3.0 - 2018-10-24

    -
    -

    Changed

    +
    +

    Changed

    • Interactive Documentation endpoint.

    • Minor improvements.

    • @@ -374,8 +419,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.2.3 - 2018-10-24

    -
    -

    Changed

    +
    +

    Changed

    • Overall improvements.

    @@ -383,8 +428,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.2.2 - 2018-10-23

    -
    -

    Added

    +
    +

    Added

    • Show traceback info when background tasks raise exceptions.

    @@ -392,8 +437,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.2.1 - 2018-10-23

    -
    -

    Added

    +
    +

    Added

    • api.requests.

    @@ -401,8 +446,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.2.0 - 2018-10-22

    -
    -

    Added

    +
    +

    Added

    • WebSocket support.

    @@ -410,8 +455,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.1.6 - 2018-10-20

    -
    -

    Added

    +
    +

    Added

    • 500 support.

    @@ -419,14 +464,14 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.1.5 - 2018-10-20

    -
    -

    Added

    +
    +

    Added

    • File upload support

    -
    -

    Changed

    +
    +

    Changed

    • Improvements to sequential media reading.

    @@ -434,8 +479,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.1.4 - 2018-10-19

    -
    -

    Fixed

    +
    +

    Fixed

    • Stability.

    @@ -443,8 +488,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.1.3 - 2018-10-18

    -
    -

    Added

    +
    +

    Added

    • Sessions support.

    @@ -452,8 +497,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.1.2 - 2018-10-18

    -
    -

    Added

    +
    +

    Added

    • Cookies support.

    @@ -461,8 +506,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.1.1 - 2018-10-17

    -
    -

    Changed

    +
    +

    Changed

    • Default routes.

    @@ -470,8 +515,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.1.0 - 2018-10-17

    -
    -

    Added

    +
    +

    Added

    • Prototype of static application support.

    @@ -479,8 +524,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.0.10 - 2018-10-17

    -
    -

    Fixed

    +
    +

    Fixed

    • Bugfix for async class-based views.

    @@ -488,8 +533,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.0.9 - 2018-10-17

    -
    -

    Fixed

    +
    +

    Fixed

    • Bugfix for async class-based views.

    @@ -497,14 +542,14 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.0.8 - 2018-10-17

    -
    -

    Added

    +
    +

    Added

    • GraphiQL Support.

    -
    -

    Changed

    +
    +

    Changed

    • Improvement to route selection.

    @@ -512,8 +557,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.0.7 - 2018-10-16

    -
    -

    Changed

    +
    +

    Changed

    • Immutable Request object.

    @@ -521,8 +566,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.0.6 - 2018-10-16

    -
    -

    Added

    +
    +

    Added

    • Ability to mount WSGI apps.

    • Supply content-type when serving up the schema.

    • @@ -531,8 +576,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.0.5 - 2018-10-15

    -
    -

    Added

    +
    +

    Added

    • OpenAPI Schema support.

    • Safe load/dump yaml.

    • @@ -541,14 +586,14 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.0.4 - 2018-10-15

    -
    -

    Added

    +
    +

    Added

    • Asynchronous support for data uploads.

    -
    -

    Fixed

    +
    +

    Fixed

    • Bug fixes.

    @@ -556,8 +601,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.0.3 - 2018-10-13

    -
    -

    Fixed

    +
    +

    Fixed

    • Bug fixes.

    @@ -565,8 +610,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.0.2 - 2018-10-13

    -
    -

    Changed

    +
    +

    Changed

    • Switch to ASGI/Starlette.

    @@ -574,8 +619,8 @@ aliases for HTTP 308 (marked for removal in 3.0)

    v0.0.1 - 2018-10-12

    -
    -

    Added

    +
    +

    Added

    • Conception!

    @@ -608,201 +653,207 @@ aliases for HTTP 308 (marked for removal in 3.0)

    • Changelog
      • Unreleased
      • -
      • v3.0.0 - 2026-03-22
          +
        • [v3.2.0] - 2026-03-22
        • -
        • v2.0.5 - 2019-12-15
            +
          • v3.0.0 - 2026-03-22 -
          • -
          • v2.0.4 - 2019-11-19 -
          • -
          • v2.0.3 - 2019-09-20
          • -
          • v2.0.2 - 2019-09-20
              -
            • Fixed
            • +
            • v2.0.5 - 2019-12-15
            • -
            • v2.0.1 - 2019-09-20
                +
              • v2.0.4 - 2019-11-19
              • +
              • v2.0.3 - 2019-09-20 +
              • +
              • v2.0.2 - 2019-09-20 +
              • +
              • v2.0.1 - 2019-09-20 +
              • v2.0.0 - 2019-09-19
              • v1.3.2 - 2019-08-15
              • v1.3.1 - 2019-04-28
              • v1.3.0 - 2019-02-22
              • v1.1.3 - 2019-01-12 -
              • -
              • v1.2.0 - 2018-12-29
              • -
              • v1.1.2 - 2018-11-11 -
              • -
              • v1.1.1 - 2018-10-29
                  +
                • v1.2.0 - 2018-12-29
                • -
                • v1.1.0 - 2018-10-27 -
                • -
                • v1.0.5- 2018-10-27
                    +
                  • v1.1.2 - 2018-11-11
                  • +
                  • v1.1.1 - 2018-10-29 +
                  • +
                  • v1.1.0 - 2018-10-27 +
                  • +
                  • v1.0.5- 2018-10-27 +
                  • v1.0.4 - 2018-10-27
                  • v1.0.3 - 2018-10-27
                  • v1.0.2 - 2018-10-27
                  • v1.0.1 - 2018-10-26 -
                  • -
                  • v1.0.0 - 2018-10-26 -
                  • -
                  • v0.3.3 - 2018-10-25
                  • -
                  • v0.3.2 - 2018-10-25
                      +
                    • v1.0.0 - 2018-10-26
                    • -
                    • v0.3.1 - 2018-10-24 -
                    • -
                    • v0.3.0 - 2018-10-24 @@ -128,10 +136,14 @@

                      G

                      @@ -141,30 +153,34 @@

                      I

                      - +
                      @@ -172,11 +188,11 @@

                      M

                      - +
                      • sse() (responder.Response method)
                      • state (responder.Request property)
                      • -
                      • status_code (responder.Response attribute) +
                      • static_app (responder.API property) +
                      • +
                      • status_code_safe (responder.Response property) +
                      • +
                      • stream() (responder.Response method)
                      • stream_file() (responder.Response method)
                      • @@ -270,6 +314,8 @@

                        T

                        diff --git a/guide-config.html b/guide-config.html new file mode 100644 index 0000000..92fae7a --- /dev/null +++ b/guide-config.html @@ -0,0 +1,264 @@ + + + + + + + + Configuration — responder 3.4.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
                        +
                        +
                        + + +
                        + +
                        +

                        Configuration

                        +

                        Every application needs different settings for different environments — +debug mode in development, real secrets in production, different database +URLs for testing. This guide covers how to manage configuration cleanly.

                        +
                        +

                        Environment Variables

                        +

                        The simplest and most universal approach. Environment variables work +everywhere — locally, in Docker, on cloud platforms — and keep secrets +out of your source code:

                        +
                        import os
                        +import responder
                        +
                        +api = responder.API(
                        +    debug=os.getenv("DEBUG", "false").lower() == "true",
                        +    secret_key=os.environ["SECRET_KEY"],
                        +    cors=os.getenv("CORS_ENABLED", "false").lower() == "true",
                        +)
                        +
                        +
                        +

                        Some variables Responder handles automatically:

                        +
                          +
                        • PORT — when set, the server binds to 0.0.0.0 on this port

                        • +
                        +

                        Set variables in your shell:

                        +
                        $ export SECRET_KEY="your-secret-here"
                        +$ export DEBUG=true
                        +$ python app.py
                        +
                        +
                        +

                        Or in a .env file (don’t commit this to git):

                        +
                        SECRET_KEY=your-secret-here
                        +DEBUG=true
                        +
                        +
                        +
                        +
                        +

                        Using .env Files

                        +

                        For local development, a .env file is convenient. Install +python-dotenv and load it at the top of your app:

                        +
                        $ uv pip install python-dotenv
                        +
                        +
                        +
                        from dotenv import load_dotenv
                        +load_dotenv()
                        +
                        +import os
                        +import responder
                        +
                        +api = responder.API(
                        +    secret_key=os.environ["SECRET_KEY"],
                        +)
                        +
                        +
                        +

                        Add .env to your .gitignore — never commit secrets.

                        +
                        +
                        +

                        Configuration Class Pattern

                        +

                        For larger applications, a configuration class keeps things organized:

                        +
                        import os
                        +
                        +class Config:
                        +    SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret")
                        +    DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
                        +    DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///dev.db")
                        +    CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "").split(",")
                        +
                        +config = Config()
                        +
                        +api = responder.API(
                        +    debug=config.DEBUG,
                        +    secret_key=config.SECRET_KEY,
                        +    cors=bool(config.CORS_ORIGINS[0]),
                        +    cors_params={"allow_origins": config.CORS_ORIGINS},
                        +)
                        +
                        +
                        +

                        This makes it easy to see all your settings in one place.

                        +
                        +
                        +

                        Secret Key

                        +

                        The secret_key is used to sign session cookies. If someone knows your +secret key, they can forge session data and impersonate any user.

                        +

                        Rules:

                        +
                          +
                        • Never use the default in production

                        • +
                        • Generate a random key: python -c "import secrets; print(secrets.token_hex(32))"

                        • +
                        • Store it in an environment variable, not in code

                        • +
                        • Rotate it if it’s ever compromised (this invalidates all sessions)

                        • +
                        +
                        api = responder.API(secret_key=os.environ["SECRET_KEY"])
                        +
                        +
                        +
                        +
                        +

                        Debug Mode

                        +

                        Debug mode controls error page behavior:

                        +
                          +
                        • On (debug=True): detailed error pages with tracebacks. Never +use this in production — it exposes your source code.

                        • +
                        • Off (debug=False): generic error pages. This is the default.

                        • +
                        +
                        api = responder.API(debug=True)  # development only
                        +
                        +
                        +

                        A common pattern is to read it from the environment:

                        +
                        api = responder.API(debug=os.getenv("DEBUG") == "true")
                        +
                        +
                        +
                        +
                        +

                        Allowed Hosts

                        +

                        In production, always set allowed_hosts to prevent Host header +attacks. This should match the domain names your application serves:

                        +
                        api = responder.API(
                        +    allowed_hosts=["example.com", "www.example.com"],
                        +)
                        +
                        +
                        +

                        In development, you can use ["*"] (the default) or specific local +addresses:

                        +
                        api = responder.API(allowed_hosts=["localhost", "127.0.0.1"])
                        +
                        +
                        +
                        +
                        +

                        Putting It All Together

                        +

                        A production-ready configuration setup:

                        +
                        import os
                        +from dotenv import load_dotenv
                        +
                        +load_dotenv()
                        +
                        +import responder
                        +
                        +api = responder.API(
                        +    debug=os.getenv("DEBUG", "false") == "true",
                        +    secret_key=os.environ["SECRET_KEY"],
                        +    allowed_hosts=os.getenv("ALLOWED_HOSTS", "*").split(","),
                        +    cors=bool(os.getenv("CORS_ORIGINS")),
                        +    cors_params={
                        +        "allow_origins": os.getenv("CORS_ORIGINS", "").split(","),
                        +        "allow_methods": ["GET", "POST", "PUT", "DELETE"],
                        +    },
                        +)
                        +
                        +
                        +

                        With a .env file for local development:

                        +
                        SECRET_KEY=dev-secret-do-not-use-in-prod
                        +DEBUG=true
                        +ALLOWED_HOSTS=localhost,127.0.0.1
                        +CORS_ORIGINS=http://localhost:3000
                        +
                        +
                        +

                        And environment variables set properly in production (via your cloud +platform’s dashboard, Docker secrets, or a secrets manager).

                        +
                        +
                        + + +
                        + +
                        +
                        + +
                        +
                        + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html index f07f730..91fa4f0 100644 --- a/index.html +++ b/index.html @@ -5,13 +5,13 @@ - Responder — responder 3.2.0 documentation + Responder — responder 3.4.1 documentation - + @@ -114,7 +114,7 @@ work with — welcome.

                        $ uv pip install responder
                         
                        -

                        Python 3.9 and above. That’s it.

                        +

                        Python 3.10 and above. That’s it.

                        diff --git a/objects.inv b/objects.inv index c9383f0..88bec76 100644 Binary files a/objects.inv and b/objects.inv differ diff --git a/py-modindex.html b/py-modindex.html index 845c0b7..dc59a5a 100644 --- a/py-modindex.html +++ b/py-modindex.html @@ -4,13 +4,13 @@ - Python Module Index — responder 3.2.0 documentation + Python Module Index — responder 3.4.1 documentation - + diff --git a/quickstart.html b/quickstart.html index 5c8e60f..82a22de 100644 --- a/quickstart.html +++ b/quickstart.html @@ -5,13 +5,13 @@ - Quick Start — responder 3.2.0 documentation + Quick Start — responder 3.4.1 documentation - + @@ -342,6 +342,51 @@ work will block the event loop. For heavy computation, consider a proper task queue.

                        +
                        +

                        Putting It All Together

                        +

                        Here’s a complete, working Responder application that combines everything +from this guide:

                        +
                        import responder
                        +
                        +api = responder.API()
                        +
                        +@api.route("/")
                        +def index(req, resp):
                        +    resp.text = "Welcome to the API"
                        +
                        +@api.route("/hello/{name}")
                        +def greet(req, resp, *, name):
                        +    resp.media = {"message": f"hello, {name}!"}
                        +
                        +@api.route("/add/{a:int}/{b:int}")
                        +def add(req, resp, *, a, b):
                        +    resp.media = {"result": a + b}
                        +
                        +@api.route("/echo", methods=["POST"])
                        +async def echo(req, resp):
                        +    data = await req.media()
                        +    resp.media = {"received": data}
                        +
                        +if __name__ == "__main__":
                        +    api.run()
                        +
                        +
                        +

                        Save this as app.py, run it with python app.py, and try:

                        +
                        $ curl http://localhost:5042/
                        +$ curl http://localhost:5042/hello/world
                        +$ curl http://localhost:5042/add/3/4
                        +$ curl -X POST http://localhost:5042/echo \
                        +    -H "Content-Type: application/json" -d '{"key": "value"}'
                        +
                        +
                        +

                        From here, explore the Feature Tour for the full range of features, or +jump into the tutorials:

                        + +
                        @@ -379,6 +424,7 @@ task queue.

                      • Reading Requests
                      • Rendering Templates
                      • Background Tasks
                      • +
                      • Putting It All Together
                      • diff --git a/sandbox.html b/sandbox.html index 9dd4113..e71e9a8 100644 --- a/sandbox.html +++ b/sandbox.html @@ -5,13 +5,13 @@ - Development Sandbox — responder 3.2.0 documentation + Development Sandbox — responder 3.4.1 documentation - + diff --git a/search.html b/search.html index 48d3503..6b397a9 100644 --- a/search.html +++ b/search.html @@ -4,14 +4,14 @@ - Search — responder 3.2.0 documentation + Search — responder 3.4.1 documentation - + diff --git a/searchindex.js b/searchindex.js index 1aad1cb..7c4cb80 100644 --- a/searchindex.js +++ b/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles":{"API Reference":[[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"]],"Blueprints \u2192 Route Groups":[[10,"blueprints-route-groups"]],"Building Frontend Assets":[[3,"building-frontend-assets"]],"Building a REST API":[[11,null]],"CORS":[[9,"cors"]],"CRUD Endpoints":[[12,"crud-endpoints"]],"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"]],"Command Line Interface":[[3,null]],"Cookie-Based Sessions":[[9,"cookie-based-sessions"]],"Cookies":[[9,"cookies"]],"Create a Book":[[11,"create-a-book"]],"Create a Web Service":[[6,"create-a-web-service"]],"Custom Error Handling":[[9,"custom-error-handling"]],"Custom Instance Names":[[3,"custom-instance-names"]],"Database Setup":[[12,"database-setup"]],"Define Your Models":[[11,"define-your-models"],[12,"define-your-models"]],"Delete a Book":[[11,"delete-a-book"]],"Deployment":[[4,null]],"Deprecated":[[2,"deprecated"]],"Development Sandbox":[[7,null]],"Docker":[[4,"docker"]],"Error Handling":[[11,"error-handling"]],"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"]],"Get a Single Book":[[11,"get-a-single-book"]],"Getting Started":[[8,"getting-started"]],"Gradual Migration":[[10,"gradual-migration"]],"GraphQL":[[9,"graphql"]],"HSTS":[[9,"hsts"]],"Hello World":[[6,"hello-world"]],"In-Memory Storage":[[11,"in-memory-storage"]],"Installation":[[5,"installation"],[12,"installation"]],"JSON APIs":[[10,"json-apis"]],"Launching from a File":[[3,"launching-from-a-file"]],"Launching from a Module":[[3,"launching-from-a-module"]],"Launching from a URL":[[3,"launching-from-a-url"]],"Lifespan Events":[[9,"lifespan-events"]],"Lifespan for Startup and Shutdown":[[12,"lifespan-for-startup-and-shutdown"]],"List All Books":[[11,"list-all-books"]],"MessagePack":[[9,"messagepack"]],"Method Filtering":[[9,"method-filtering"]],"Migrating from Flask":[[10,null]],"Mounting Other Apps":[[9,"mounting-other-apps"]],"OpenAPI Documentation":[[9,"openapi-documentation"]],"Operations":[[7,"operations"]],"Project":[[5,null]],"Project Setup":[[11,"project-setup"]],"Quick Reference":[[10,"quick-reference"]],"Quick Start":[[6,null]],"Rate Limiting":[[9,"rate-limiting"]],"Reading Requests":[[6,"reading-requests"]],"Removed":[[2,"removed"]],"Rendering Templates":[[6,"rendering-templates"]],"Request":[[0,"request"]],"Request ID":[[9,"request-id"]],"Responder":[[5,null]],"Response":[[0,"response"]],"Reverse Proxy":[[4,"reverse-proxy"]],"Route Groups":[[9,"route-groups"]],"Route Parameters":[[6,"route-parameters"],[10,"route-parameters"]],"Run It":[[11,"run-it"],[12,"run-it"]],"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"]],"Status Code Helpers":[[0,"status-code-helpers"]],"Templates":[[10,"templates"]],"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 API Class":[[0,"the-api-class"]],"The Big Differences":[[10,"the-big-differences"]],"The Idea":[[5,"the-idea"]],"Tips":[[8,"tips"],[12,"tips"]],"Trusted Hosts":[[9,"trusted-hosts"]],"Try It Out":[[11,"try-it-out"]],"Tutorials":[[5,null]],"Type Convertors":[[6,"type-convertors"]],"Unreleased":[[2,"unreleased"]],"Update a Book":[[11,"update-a-book"]],"User Guide":[[5,null]],"Using Fixtures":[[8,"using-fixtures"]],"Using PostgreSQL":[[12,"using-postgresql"]],"Using SQLAlchemy":[[12,null]],"Uvicorn Directly":[[4,"uvicorn-directly"]],"WebSocket Support":[[9,"websocket-support"]],"What You Get":[[5,"what-you-get"]],"What\u2019s Next":[[11,"what-s-next"]],"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","tutorial-flask","tutorial-rest","tutorial-sqlalchemy"],"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","tutorial-flask.rst","tutorial-rest.rst","tutorial-sqlalchemy.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.status_codes)":[[0,"responder.status_codes.is_100",false]],"is_200() (in module responder.status_codes)":[[0,"responder.status_codes.is_200",false]],"is_300() (in module responder.status_codes)":[[0,"responder.status_codes.is_300",false]],"is_400() (in module responder.status_codes)":[[0,"responder.status_codes.is_400",false]],"is_500() (in module responder.status_codes)":[[0,"responder.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.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,4,1,"","content"],[0,4,1,"","cookies"],[0,2,1,"","file"],[0,4,1,"","formats"],[0,4,1,"","headers"],[0,4,1,"","media"],[0,4,1,"","session"],[0,2,1,"","sse"],[0,4,1,"","status_code"],[0,2,1,"","stream_file"]],"responder.status_codes":[[0,5,1,"","is_100"],[0,5,1,"","is_200"],[0,5,1,"","is_300"],[0,5,1,"","is_400"],[0,5,1,"","is_500"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","property","Python property"],"4":["py","attribute","Python attribute"],"5":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:method","3":"py:property","4":"py:attribute","5":"py:function"},"terms":{"":[0,2,3,4,5,6,8,9,10,12],"0":[3,4,6,9,11,12],"0441172719":11,"1":[0,3,4,6,9,10,11],"10":[0,6,9],"100":9,"127":[3,4],"13":4,"150mb":4,"1965":11,"2":[4,6,8,9,11,12],"200":[6,8,9,11],"201":[6,8,10,11,12],"204":[11,12],"3":[4,5,6,8,9,11],"301":[0,6],"308":2,"3600":9,"4":4,"40":2,"400":[0,8,9,11],"401":9,"404":[6,10,11,12],"41d4":6,"422":[8,9,11],"429":9,"446655440000":6,"500":[2,6,8,9,11],"5000":9,"5042":[3,4,6,11],"550e8400":6,"60":9,"600":[0,9],"8":0,"80":4,"8000":[4,6],"8192":0,"9":[5,8],"978":11,"99":8,"A":[0,4,5,6,9],"And":[5,6],"As":9,"But":[6,8,9],"By":[3,6,8,9,11],"For":[0,3,4,6,8,9,11,12],"If":[0,3,5,6,8,9,10,12],"In":[5,6,9,10,12],"It":[0,3,5,6,8,9],"No":[5,8,9,10,11],"Not":6,"On":9,"One":[5,6,8],"That":[4,5,6,8],"The":[2,3,4,6,8,9,11,12],"There":[8,9],"These":6,"To":[11,12],"Will":0,"With":0,"__main__":[4,5,8,11,12],"__name__":[4,5,8,9,10,11,12],"__tablename__":12,"_route_for":2,"a2wsgi":2,"a716":6,"abc":11,"abc123":[8,9],"abil":2,"abl":0,"abort":10,"about":[4,6,8,10],"abov":5,"abstract":12,"accept":[0,2,6,8,9],"access":[0,4,9,12],"acm":3,"acquir":7,"across":[4,9,12],"activ":7,"actual":[0,6,9],"ad":[1,9],"adapt":9,"add":[0,1,2,6,11,12],"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,6,9],"adher":2,"affect":9,"after":[0,5,6,12],"after_request":[0,1,8,9],"ag":9,"against":[0,4,9],"agre":6,"ai":9,"aiosqlit":12,"alemb":12,"alias":2,"alic":9,"all":[0,2,3,4,5,6,7,8,9,10,12],"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,10,11,12],"angle_bracket":10,"angular":9,"ani":[0,3,4,5,6,8,9,11,12],"annot":[5,6,9],"annual":[6,9],"answer":9,"anyon":5,"anyth":6,"anywher":[4,9],"api":[2,3,4,5,6,9,12],"api_v1":10,"apispec":2,"app":[0,2,3,4,5,8,10,11,12],"appar":0,"apparent_encod":0,"appear":9,"applic":[0,2,3,4,6,8,9,11,12],"approach":[9,10],"appropri":[9,12],"ar":[0,2,3,4,5,6,8,9,10,12],"architectur":9,"aren":6,"arg":[0,10],"argument":[0,2,6,10],"around":[0,5],"arriv":[6,10],"asgi":[0,2,4,5,6,9,10],"ask":[5,9],"assert":8,"asset":[4,5,9],"assign":11,"async":[0,1,2,4,5,6,8,9,10,11,12],"async_sess":12,"async_sessionmak":12,"asynccontextmanag":[9,12],"asynchron":[2,4],"asyncio":12,"asyncpg":12,"attach":9,"attack":[4,9],"attribut":[0,3,12],"auth":[5,9],"auth_check":9,"authent":[6,9],"author":[6,7,9,11,12],"auto":9,"auto_escap":0,"autobuild":7,"autoincr":12,"automat":[0,4,5,6,8,9,10,11,12],"avail":6,"avoid":[8,9],"aw":4,"await":[0,6,8,9,10,11,12],"azur":[3,4],"b":[6,8,12],"back":[0,6,9,12],"background":[2,5],"backlog":5,"bad":[8,9,11],"balanc":4,"bandwidth":9,"bar":9,"base":[0,2,5,12],"base_url":0,"basemodel":[0,8,9,11,12],"basic":6,"batteri":5,"battl":5,"becaus":[0,5,6,10,11],"becom":[0,12],"befor":[5,6,12],"before_request":[0,1,2,8,9,10],"begin":12,"behind":[4,9],"being":0,"best":5,"better":5,"bidirect":9,"big":5,"biggest":10,"bin":7,"binari":[6,9],"bind":[0,4,6],"blob":3,"block":[6,8,9,10],"blueprint":5,"bodi":[0,6,8,9,10,11],"boilerpl":5,"book":[5,12],"book_id":[11,12],"bookin":[11,12],"bookout":12,"books_db":11,"born":5,"both":[5,6,9,10],"bottom":11,"bp":10,"break":8,"bring":5,"broke":[6,8],"broker":6,"browser":[6,7,9,11],"bucket":9,"buffer":6,"bufix":2,"bug":[2,8],"bugfix":2,"build":[2,4,5,6,7,12],"builder":12,"built":[2,4,5,6,8,9],"bump":2,"bypass":4,"byte":[0,5,6],"cach":[6,8,9],"caddi":4,"call":[3,6,8,9,11],"callabl":0,"came":6,"can":[0,3,4,5,6,8,9,10,11,12],"carri":6,"case":[0,5,6],"cat":9,"catalog":11,"catch":9,"categori":0,"cd":7,"ceas":2,"celeri":6,"central":[0,6],"certain":0,"certif":4,"chang":12,"changelog":5,"channel":9,"chardet":0,"chat":9,"check":[0,7,8,9],"check_exist":[0,9,11,12],"choic":[6,9],"choos":9,"chunk":[0,9],"chunk_siz":0,"ci":2,"circuit":[1,5,9],"class":[2,5,6,8,11,12],"clean":[5,9,11],"cleaner":9,"cleanup":9,"cli":2,"click":5,"client":[0,5,6,8,9,11],"clone":7,"close":[8,9,12],"close_database_connection_pool":0,"cloud":[3,5,6],"cmd":4,"code":[2,3,4,5,6,7,8,9,11,12],"collabor":9,"colon":3,"column":12,"com":[3,6,7,9],"come":[6,9,10],"command":[4,5],"commit":12,"common":[3,4,6,9,10,11],"commonli":6,"commun":9,"compact":9,"complement":[1,9],"complet":[9,11],"complex":12,"compos":12,"compress":[5,6],"comput":6,"concept":[2,9,10],"concern":6,"concurr":4,"condit":[8,9],"config":12,"configur":[0,2,4,6,8,9],"conflict":2,"confus":8,"conn":12,"connect":[0,4,8,9,12],"consid":[1,6,12],"consist":[6,9],"consol":[9,12],"const":9,"consum":9,"contact":0,"contain":[0,4,6],"container":4,"content":[0,2,5,6,8,9,11],"content_typ":[0,8,9],"context":[5,8,9,12],"contextlib":[9,12],"continu":6,"contract":8,"control":[0,4,6,9,12],"conveni":[0,12],"convent":[4,11],"convert":[2,6],"convertor":[5,10],"cooki":[0,2,5,6,10],"copi":4,"cor":[0,2,5,8],"core":[2,4],"coroutin":0,"correctli":4,"correspond":12,"cors_param":[0,9],"count":2,"cpu":[4,6],"creat":[0,5,7,8,9,10,12],"create_al":12,"create_async_engin":12,"create_book":[11,12],"create_item":[0,9,10],"create_pet":9,"creation":12,"cross":9,"crud":[5,11],"css":9,"csv":9,"curl":[3,6,11],"curly_brac":10,"custom":[0,5,6,8,11],"cycl":6,"d":[8,11],"dashboard":9,"data":[0,2,6,8,9,10,11,12],"databas":[5,6,8,9,11],"database_url":12,"db":12,"debug":[0,2,9,10],"decim":6,"declar":[2,6],"declarativebas":12,"decod":[2,9],"decor":[0,6,9],"deep":5,"def":[0,5,6,8,9,10,11,12],"default":[0,1,2,3,6,8,9,12],"default_valu":[0,9],"defend":9,"defin":[0,5,6,9],"definit":0,"del":11,"delet":[5,6,9,12],"delete_book":[11,12],"deliber":6,"demo":3,"denpend":2,"depend":[2,4,8],"deploi":[4,5],"deploy":5,"depth":9,"describ":9,"descript":[0,9],"design":[6,9,11],"detail":[4,9,11],"detect":[0,5,9],"dev":2,"develop":[3,4,9,12],"devic":9,"dict":[0,5,6,9,11,12],"dictionari":0,"differ":[3,5,8,9,12],"digit":6,"direct":[2,9],"directli":[3,5,6,8,11,12],"directori":[0,6,9],"disabl":[8,12],"disk":[0,6,9],"dispatch":[2,9],"dispos":12,"distribut":[4,9],"django":[5,9],"do":[4,6,9],"doc":[7,8,9,11],"docker":5,"dockerfil":4,"docs_rout":[0,9,11],"docstr":9,"document":[0,2,4,5,7,11],"doe":12,"doesn":[0,9,11,12],"domain":9,"don":[6,8,9,10,12],"done":4,"dot":3,"doubl":11,"down":[9,12],"download":9,"dramat":2,"driver":12,"drop":2,"dump":2,"dune":11,"duplex":9,"duplic":9,"dure":[3,4,8,12],"duti":11,"dynam":6,"e":0,"e29b":6,"each":[0,4,8,9,12],"easi":[3,6],"echo":12,"ecosystem":[5,6],"edit":7,"editor":9,"effect":[8,11],"effici":4,"either":0,"element":[0,9],"els":[6,9,10,12],"email":6,"emb":9,"empti":11,"enabl":[2,9,11],"enable_async":6,"enable_hst":[0,9],"encod":0,"end":[6,11],"endpoint":[0,2,5,8,9,10,11],"enforc":9,"engin":[6,12],"enough":4,"ensur":[9,11,12],"enter":8,"entir":[0,9,11],"entri":11,"env":4,"environ":[0,3,4,6],"eol":2,"equival":10,"error":[0,5,12],"errorhandl":10,"escap":0,"essenti":9,"etc":[6,8,9],"even":[3,4,9],"event":[0,2,5,6,10],"event_typ":0,"eventsourc":9,"ever":5,"everi":[0,4,5,6,8,9,11,12],"everyth":[0,4,9,10,11,12],"everywher":3,"evolv":12,"exactli":[8,9],"exampl":[0,3,6,9],"exc":[0,8,9,11],"exceed":9,"except":[0,2,5,8,9],"exception_cl":0,"exception_handl":[0,8,9,10,11],"execut":12,"exist":[0,2,5,10],"exit":[8,9],"expect":6,"expir":9,"expire_on_commit":12,"explain":9,"explan":0,"explicit":[6,9,10],"explicitli":[9,10],"explor":[1,9],"export":9,"expos":4,"expose_head":0,"express":[5,6],"ext":[2,9,12],"extens":[2,6],"extern":6,"extra":[2,4,9],"extract":0,"extrem":6,"f":[0,5,6,8,9,11],"factori":12,"fail":[8,9,12],"failur":8,"falcon":[5,9],"fall":0,"fals":[0,8,9,11,12],"familiar":[5,10],"fast":[4,5,6,8],"fastapi":5,"faster":9,"featur":[0,5,6],"feed":9,"feel":5,"fetch":3,"few":10,"fido":9,"field":[0,8,9,11],"figur":6,"file":[0,2,4,5,6,11,12],"filenam":[0,8],"filepath":6,"filesystem":[2,3],"fill":6,"filter":[5,6],"find":[3,9,10],"fine":[4,9,12],"fire":8,"first":[0,9,11,12],"fit":5,"fix":7,"fixtur":5,"flag":[4,9,12],"flask":[5,9],"flask_app":[9,10],"float":[0,6,8],"fly":4,"follow":6,"font":9,"forg":9,"form":[0,6,10],"format":[0,2,6,7,9],"formerli":9,"forward":[4,9],"found":[2,6,11,12],"foundat":9,"fragment":3,"framework":[5,9],"franca":6,"frank":11,"fresh":8,"from":[0,2,4,5,6,8,9,11,12],"from_attribut":12,"front":4,"frontend":[5,9],"fsspec":3,"full":[0,2,3,6,9],"full_url":[0,6,9],"fulli":6,"fun":5,"func":0,"function":[0,6,9,10],"futur":5,"g":0,"game":9,"gatewai":4,"gener":[0,5,8,9,11],"get":[0,6,9,12],"get_book":[11,12],"get_us":[0,9,10],"git":7,"github":[3,7],"give":[0,8,9,12],"given":[0,8],"global":[10,11],"go":[6,10],"goe":[9,10],"good":[4,5,9],"googl":[3,4],"grace":4,"grade":6,"gradual":[5,9],"grain":12,"graphen":[0,2,5,9],"graphiql":[2,5,9],"graphql":[0,2,5],"great":[3,5,9],"greet":[5,9],"greet_world":5,"greetingresourc":9,"group":[0,5],"grow":9,"guard":5,"guid":[6,10,12],"guido":6,"gzip":[5,6],"h":11,"h1":6,"ha":[6,8,10,11],"half":6,"halv":9,"hand":0,"handl":[4,5,6],"handle_value_error":[0,9,11],"handler":[0,5,8,9,11],"happen":[5,6],"hard":8,"have":[0,2,3,6,8,10,11],"hdf":3,"head":3,"header":[0,4,5,6,9,10],"heavi":6,"hello":[0,3,5,8,9,10],"hello_html":6,"hello_json":6,"hello_to":6,"hello_world":6,"helloworld":3,"help":[0,12],"helper":5,"herbert":11,"here":[4,6,9,10],"high":[4,9],"hold":[0,5,6],"home":5,"honor":[4,6],"hood":9,"hook":[0,1,5,6],"host":[0,2,4,5,6],"hostnam":9,"hour":9,"how":[5,6,9,10,11,12],"hst":5,"html":[0,2,5,6,9,10],"http":[0,2,3,4,5,6,7,8,9,11],"httponli":9,"httpsredirectmiddlewar":4,"httpx":[2,8],"i":[0,2,3,4,5,6,8,9,10,11,12],"id":[0,5,6,10,11,12],"idea":10,"idempot":11,"ident":10,"identifi":9,"imag":[4,6,9],"immedi":[5,6,9],"immut":2,"implement":8,"import":[0,2,3,5,6,8,9,10,11,12],"improv":2,"includ":[3,4,5,6,7,8,9,11],"incom":[0,6,11],"incredibli":9,"independ":4,"index":[0,9],"individu":9,"industri":9,"info":[0,2,9],"inform":[0,11],"infrastructur":[4,5],"inherit":9,"initi":[0,8],"inject":9,"input":[8,9,11],"insensit":[0,5,6],"instal":[0,3,4,7,9],"instanc":[0,4,5,6,8],"instant":6,"instead":[6,8,9,11,12],"int":[0,6,9,10,11,12],"integ":[0,6,11,12],"integr":[11,12],"intens":6,"intent":5,"intention":9,"interact":[2,9,11],"interfac":[4,5,6,10],"intern":[8,9],"invalid":[8,9],"involv":0,"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,"isbn":[11,12],"isn":[6,9,10],"issu":2,"item":[0,8,9,10,12],"itemin":0,"itemout":0,"its":[0,2,4,11,12],"j":9,"javascript":[3,9],"jinja2":[0,6,10],"jpeg":9,"jpg":9,"json":[0,3,5,6,9,11],"jsonifi":10,"just":[4,8,9,12],"keep":[2,6,8,9,12],"kei":[0,8,9,10,12],"kennethreitz":[3,7],"keyword":[0,6,10],"know":6,"known":0,"kwarg":0,"languag":9,"larg":9,"larger":8,"later":[6,8],"launch":[4,5],"lazili":0,"lead":8,"learn":9,"legaci":10,"less":9,"let":[3,4,6,9,11,12],"level":4,"librari":[8,9],"licens":0,"lifespan":[0,2,5,11],"lift":6,"lightn":4,"lightweight":6,"like":[4,6,8,9,11],"limit":[1,4,5,12],"line":[5,6],"lingua":6,"list":[0,5,6,8,9,12],"list_book":[11,12],"list_item":9,"list_pet":9,"list_us":[0,9,10],"list_users_v2":9,"live":9,"ll":[5,6,9,10,11,12],"load":[0,2,3,4,6,9],"local":[3,5,10],"localhost":[6,11,12],"locat":[0,6,10],"log":[4,9],"log_level":2,"log_respons":9,"logic":[5,6,9],"login":9,"long":6,"look":[3,5,6],"loop":[6,10],"lose":12,"lower":0,"lowercas":6,"machin":9,"made":5,"magic":10,"mai":4,"main":3,"make":[4,6,8,9,10],"malici":9,"manag":[5,8,9,12],"mani":[4,6,9],"manual":0,"map":[2,6,9,10,12],"mapper":12,"mark":[2,9],"marshmallow":[0,2,9],"match":[0,6,9,11],"matter":9,"max_ag":[0,9],"mean":[4,6,10],"mechan":9,"media":[0,2,5,6,8,9,10,11,12],"memori":[0,5,6,9],"messag":[0,6,9],"messagepack":5,"metadata":[6,12],"method":[0,5,6,8,10,11,12],"microservic":9,"middelwar":0,"middlewar":[0,1,2,6,9],"might":6,"migrat":[2,5,9,12],"mime":0,"mimetyp":[6,9],"min":9,"minim":4,"minimum":2,"minor":2,"miss":[8,9],"mix":9,"ml":9,"mode":[0,7],"model":[0,5,8,9],"model_dump":12,"model_valid":12,"modern":[4,9,10,12],"modifi":[9,11,12],"modul":[0,2,5,6,9],"more":[0,3,4,6,9,10,11],"most":[0,3,4,6,8,9,12],"mount":[0,2,5,10],"move":[2,6,9,10],"msgpack":9,"much":[6,9],"multipart":[2,6],"multipl":[2,4,9,11],"multiplay":9,"must":[0,6,9,11],"mutabl":5,"mutat":[0,6,10],"my_templ":6,"myapi":4,"myapp":3,"mydb":12,"mysql":12,"n":6,"name":[0,5,6,8,9,10],"namespac":2,"nativ":10,"natur":[8,12],"nearli":10,"need":[4,5,6,8,9,10,12],"negoti":[0,5,6,8,9],"nervou":6,"network":8,"never":[9,11],"new":[0,6,8,9,10,11],"new_endpoint":10,"next":5,"next_id":11,"nginx":4,"non":6,"none":[0,2,11,12],"normal":9,"notabl":2,"notasecret":0,"note":[6,9,10,11],"notic":6,"notif":9,"now":[2,6,9,12],"npm":3,"nullabl":12,"number":6,"o":[0,6,10],"object":[0,2,6,9,10,12],"objecttyp":[0,9],"obviou":8,"off":6,"offload":4,"offset":12,"often":[3,6,9],"ok":[6,8,11],"old":10,"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,10,11],"one":[0,5,6,9,10,12],"ones":[5,6,9],"onli":[6,9,10,11],"onmessag":9,"open":[2,6,7,12],"open_database_connection_pool":0,"openapi":[0,2,5,11],"openapi_rout":0,"openapi_them":0,"oper":[6,9,10,11],"opt":9,"option":[0,2,4,5,6],"order_bi":12,"organ":[8,9,10],"origin":9,"orm":12,"other":[0,2,5,6,12],"otherwis":[0,9],"our":11,"out":[5,6],"outgo":6,"output":11,"over":[0,5,6,9,10,12],"overal":2,"overhead":8,"overrid":[0,9],"overwhelm":9,"own":[4,9,12],"p":4,"pace":9,"packag":[2,3,4],"page":[0,2,6,9],"pagin":6,"painless":8,"param":[0,2,5,6,10],"paramet":[0,5,8,9,11],"parameter":0,"pars":[0,2,6,8,9],"part":[5,6,9],"pass":[0,6,8,10,12],"passion":5,"patch":[6,9],"path":[0,2,3,5,6,8,9],"path_matches_rout":0,"path_param":[0,6],"pattern":[4,5,6,9,10,11,12],"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,12],"place":9,"placehold":6,"plain":[6,9],"platform":[2,5,6],"pleasant":5,"plu":12,"plugin":2,"point":[3,4,11],"polici":9,"pool":[5,6,9,10,12],"popular":[6,9,12],"port":[0,4,6,8],"portion":0,"possibl":6,"post":[0,6,8,9,10,11,12],"postgresql":5,"potenti":2,"power":[5,8,9,12],"practic":9,"prefer":9,"prefix":[0,9],"present":6,"prevent":9,"price":[0,8],"primari":0,"primary_kei":12,"print":[9,12],"privat":9,"process":[4,5,6,8,9],"process_data":6,"produc":6,"product":[3,4,5,6,8,9,12],"profil":9,"programmat":9,"progress":9,"project":[2,3,7],"propag":8,"proper":[6,12],"properli":[8,12],"properti":[0,6,8],"protect":4,"proto":4,"protocol":[3,6,8,9],"prototyp":[2,3,5],"provid":0,"proxi":5,"public":[0,9],"push":[4,5,9],"put":[6,9,11,12],"py":[2,3,4,8,11,12],"pydant":[0,8,9,11,12],"pyproject":2,"pytest":[5,7,8],"python":[0,2,3,4,5,6,9,11,12],"pythonpath":3,"q":[6,10],"queri":[0,5,6,9,12],"queue":6,"quick":[0,3,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,12],"re":[0,4,6,8,9,10,11],"react":[2,9],"read":[0,2,5,9,10,11,12],"readabl":9,"readi":[4,5,6],"real":[9,11,12],"realli":[8,9],"receiv":[0,5,6,8,9],"receive_byt":9,"receive_incom":6,"receive_json":9,"receive_text":[8,9],"recommend":9,"reconnect":9,"redirect":[0,2,6,10],"redoc":[0,9],"reduc":2,"ref":3,"refactor":[2,8],"refer":[5,8],"refresh":12,"refus":9,"regist":[0,8,9],"register_blueprint":10,"reject":[8,9],"relat":[9,12],"releas":7,"reliabl":8,"remain":9,"remov":[9,10,11],"renam":8,"render":[0,1,4,5,9],"render_async":6,"render_templ":10,"replac":[2,6,9,11],"report":[6,8,9],"repres":0,"represent":0,"req":[0,5,6,8,9,10,11,12],"request":[2,3,4,5,10,11,12],"request_id":[0,9],"request_model":[0,8,9,11,12],"requir":[0,2,8,9],"research":5,"resolv":[2,9],"resolve_hello":[0,9],"resourc":[6,9,11],"resp":[0,2,5,6,8,9,10,11,12],"respect":3,"respond":[0,2,3,4,6,7,8,9,10,11,12],"respons":[5,8,9,10,11,12],"response_model":[0,9,11,12],"rest":[4,5,9],"restrict":9,"result":[9,12],"resum":2,"resume_incomplet":2,"retri":[0,9],"retriev":[9,11],"return":[0,2,5,6,8,9,10,11,12],"revers":5,"rfc3986":2,"right":[5,6,9,10],"roll":12,"rout":[0,2,5,8,11,12],"router":2,"ruff":7,"run":[0,2,3,5,7,8,9,10],"run_sync":12,"runner":4,"s3":3,"safe":[2,11],"sai":6,"said":4,"same":[3,4,5,6,9,10,11,12],"sandbox":5,"scalar":12,"scale":5,"schema":[0,2,5,9,11,12],"scope":[0,9],"scratch":[5,11],"search":[0,2,6,9],"second":0,"secret":9,"secret_kei":[0,9],"section":9,"secur":9,"see":[0,4,6,8,9,11,12],"select":[0,2,12],"self":[0,9],"semant":2,"send":[0,2,5,8,9,11],"send_byt":9,"send_json":9,"send_text":[8,9],"sent":[0,5,6],"separ":[6,8,11],"sequenti":2,"serial":[6,9],"serializ":6,"serv":[0,2,3,4,5,6,8,11],"server":[0,2,3,4,5,8,11,12],"servestat":2,"servic":[0,3,5,8,9,11],"session":[0,2,5,6,8,10,12],"session_id":9,"set":[0,2,4,5,6,7,8,9,12],"set_cooki":[2,9],"set_text":0,"setattr":12,"setup":[2,5,8],"sever":9,"sftp":3,"share":[0,3,6,8,9,12],"short":[1,5,9],"should":[11,12],"shouldn":0,"show":[2,9,10,12],"shut":[9,12],"shutdown":[0,4,5,8,9],"shutdwown":2,"sibl":5,"side":[6,9],"sign":[5,6,9],"signatur":[6,9,10],"simpl":[3,8,9,11],"simplecooki":0,"simpler":[6,9],"simplest":6,"simplic":[5,12],"simul":6,"sinc":[6,8],"singl":[2,3,5,6,9,10,12],"size":[0,12],"skip":9,"slash":6,"sleep":[6,8],"slim":4,"slow":6,"slowlori":4,"slug":6,"small":[4,5,8,9],"smaller":4,"so":[8,9,10,11,12],"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":[5,9],"specif":[0,8,9,11],"specifi":3,"sphinx":7,"spin":6,"sprawl":5,"sql":12,"sqlalchemi":[5,11],"sqlite":12,"sse":[0,5],"ssl":4,"stabil":2,"stai":[10,12],"standard":[3,4,9,11],"starlett":[0,2,5,8],"start":[0,3,4,5,9,11,12],"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":[2,5,6,8,9,11],"status_cod":[0,5,6,8,9,10,11,12],"stdlib":2,"still":[9,10],"stop":9,"storag":[3,5],"store":[0,9,11],"str":[0,6,8,9,11,12],"straightforward":9,"stranger":[0,9],"stream":[0,2,9],"stream_fil":[0,9],"strict":9,"string":[0,5,6,9,12],"strip":9,"structur":9,"style":9,"stylesheet":9,"subcommand":3,"subrout":[5,9,10],"subsequ":9,"subtl":2,"success":[6,11],"successfulli":11,"successor":4,"suit":8,"suppli":2,"support":[0,1,2,3,4,5,6,10,12],"swagger":[5,9,11],"swagger_ui":[0,9],"switch":[2,12],"sync":[2,5,6,10],"synchron":[0,10],"syntax":[5,6,10],"system":[3,6,9],"t":[0,4,6,8,9,10,11,12],"tabl":12,"take":[5,6],"tamper":9,"target":2,"task":[2,5],"teach":5,"tear":9,"tell":[6,9,11,12],"templat":[0,1,2,5],"template_str":[0,6],"templates_dir":0,"termin":[3,4],"terms_of_servic":0,"test":[0,2,3,5,7,11],"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,5,6,8,9,10],"than":[8,9,10,11],"thei":[6,9,10,11],"them":[5,6,8,9,10,11],"theme":[0,9],"themselv":9,"thi":[0,2,3,4,5,6,8,9,10,11,12],"thing":[5,6,8],"think":6,"though":4,"thousand":4,"thread":[5,6,10],"threadpoolexecutor":2,"three":[6,9],"through":[6,9,11],"throughput":9,"time":[0,5,6,8,9,10,11,12],"time_start":0,"timeout":4,"tip":5,"titl":[0,6,9,11,12],"tl":4,"togeth":[5,9],"token":[6,9],"toml":2,"too":[3,4,8,9],"tool":7,"toolbelt":2,"toolkit":12,"top":0,"total":9,"tour":[0,5],"trace":9,"traceback":[2,9],"track":12,"tradit":9,"traffic":4,"transact":12,"translat":10,"transport":9,"treat":[0,9],"trick":9,"trigger":8,"true":[0,6,8,9,10,12],"trust":5,"trustedhostmiddlewar":4,"try":5,"tupl":[0,6,8],"tutori":11,"two":[6,9],"type":[0,2,5,9,10,11],"typic":[6,9],"typo":[2,9],"ui":[2,5,9,11],"unauthor":9,"under":[6,9,10],"understand":[5,6],"unicod":0,"uniqu":9,"unit":12,"unknown":0,"unless":9,"unlik":9,"unmaintain":2,"unmatch":9,"unpin":2,"unrecogn":9,"until":6,"up":[0,2,6,7,9],"updat":[2,5,6,9,12],"update_book":[11,12],"upgrad":[2,7,9],"upload":[2,5,6],"url":[0,2,5,6,8,9,10,12],"url_for":[0,8],"url_prefix":10,"urllib":2,"us":[0,2,3,4,5,6,9,10,11],"usag":[0,2],"user":[0,6,9,10,12],"user_id":[6,9,10],"usernam":9,"usual":8,"utf":0,"uuid":[0,6,9],"uuid4":0,"uv":[3,5,7,12],"uvicorn":[0,2,5,6],"uvloop":4,"v1":[0,9],"v2":9,"valid":[5,6,9,11,12],"valu":[0,5,6,8,9,10,11,12],"valueerror":[0,8,9,10,11],"variabl":[0,4,6],"ve":[5,8,10],"venv":7,"verb":9,"veri":[0,6,12],"verifi":8,"version":[0,2,8,9,11],"via":[2,9,10],"view":[0,2,5,6,8,10],"virtual":[3,4],"virtualenv":7,"visit":[9,11],"vue":9,"w":[8,9],"wa":[0,2,6,11],"wai":[3,4,5,6,9,10,12],"wait":10,"walk":[6,9,11],"want":[0,3,4,5,6,8,9,11,12],"watch":7,"we":[11,12],"web":[0,4,5,9,12],"websit":9,"websocket":[0,1,2,4,5],"websocket_connect":8,"welcom":5,"well":5,"went":5,"what":[0,6,8,9],"whatev":6,"when":[2,4,6,8,9,10,11,12],"where":[4,6,9,12],"whether":[0,6],"which":[0,2,6,8,9,10,12],"whichev":5,"while":[6,9,10],"whitenois":2,"who":[5,6],"widget":8,"wildcard":9,"window":2,"wire":6,"within":[2,12],"without":[0,4,6,8,9,12],"won":[6,8,9,12],"work":[3,4,5,6,9,11,12],"workdir":4,"worker":4,"world":[0,3,5,8,9,10],"worri":8,"would":8,"wrap":[6,8,9],"write":[5,6,9,10],"wrong":9,"wsgi":[0,2,4,5,9,10],"x":[0,4,6,8,9,10,11],"x89png":6,"xml":0,"xss":9,"yaml":[0,2,5,6,8,9],"year":[11,12],"yield":[0,9,12],"yml":[0,9],"you":[0,3,4,6,8,9,10,11,12],"your":[0,3,4,5,6,8,9,10],"yourself":6,"zero":4},"titles":["API Reference","Backlog","Changelog","Command Line Interface","Deployment","Responder","Quick Start","Development Sandbox","Testing","Feature Tour","Migrating from Flask","Building a REST API","Using SQLAlchemy"],"titleterms":{"":11,"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,"In":11,"It":[11,12],"The":[0,5,10],"ad":2,"after":[8,9],"all":11,"api":[0,8,10,11],"app":9,"asset":3,"background":6,"backlog":1,"base":9,"befor":[8,9],"big":10,"blueprint":10,"book":11,"build":[3,11],"chang":2,"changelog":2,"class":[0,9],"cloud":4,"code":0,"command":3,"convertor":6,"cooki":[8,9],"cor":9,"creat":[6,11],"crud":12,"custom":[3,9],"databas":12,"defin":[11,12],"delet":11,"deploy":4,"deprec":2,"develop":7,"differ":10,"directli":4,"docker":4,"document":9,"endpoint":12,"error":[8,9,11],"event":[8,9],"featur":9,"file":[3,8,9],"filter":9,"fix":2,"fixtur":8,"flask":10,"from":[3,10],"frontend":3,"futur":1,"get":[5,8,11],"gradual":10,"graphql":9,"group":[9,10],"guid":5,"handl":[8,9,11],"header":8,"hello":6,"helper":0,"hook":[8,9],"host":9,"hst":9,"id":9,"idea":[1,5],"instal":[5,12],"instanc":3,"interfac":3,"json":[8,10],"launch":3,"lifespan":[8,9,12],"limit":9,"line":3,"list":11,"local":4,"memori":11,"messagepack":9,"method":9,"migrat":10,"model":[11,12],"modul":3,"mount":9,"name":3,"next":11,"openapi":9,"oper":7,"other":9,"out":11,"paramet":[6,10],"platform":4,"postgresql":12,"project":[5,11],"proxi":4,"quick":[6,10],"rate":9,"read":6,"refer":[0,10],"remov":2,"render":6,"request":[0,6,8,9],"respond":5,"respons":[0,6],"rest":11,"revers":4,"rout":[6,9,10],"run":[4,6,11,12],"sandbox":7,"send":6,"sent":9,"serv":9,"server":[6,9],"servic":6,"session":9,"setup":[7,11,12],"shutdown":12,"singl":11,"sqlalchemi":12,"sse":9,"start":[6,8],"startup":12,"static":9,"statu":0,"storag":11,"support":9,"task":6,"templat":[6,10],"test":8,"tip":[8,12],"tour":9,"trust":9,"try":11,"tutori":5,"type":6,"unreleas":2,"updat":11,"upload":8,"url":3,"us":[8,12],"user":5,"uvicorn":4,"v0":2,"v1":2,"v2":2,"v3":2,"valid":8,"view":9,"web":6,"websocket":[8,9],"what":[5,11],"world":6,"you":5,"your":[11,12]}}) \ No newline at end of file +Search.setIndex({"alltitles":{"API Key Authentication":[[11,"api-key-authentication"]],"API Reference":[[0,null]],"Added":[[2,"added"],[2,"id1"],[2,"id4"],[2,"id10"],[2,"id11"],[2,"id14"],[2,"id17"],[2,"id22"],[2,"id29"],[2,"id35"],[2,"id36"],[2,"id37"],[2,"id38"],[2,"id39"],[2,"id42"],[2,"id43"],[2,"id45"],[2,"id48"],[2,"id51"],[2,"id52"],[2,"id53"],[2,"id57"]],"Adding Third-Party Middleware":[[13,"adding-third-party-middleware"]],"After-Request Hooks":[[10,"after-request-hooks"]],"Allowed Hosts":[[5,"allowed-hosts"]],"Authentication":[[11,null]],"Background Queue":[[0,"background-queue"]],"Background Tasks":[[7,"background-tasks"]],"Backlog":[[1,null]],"Bearer Token Authentication":[[11,"bearer-token-authentication"]],"Before-Request Hooks":[[10,"before-request-hooks"]],"Before-Request Hooks for WebSockets":[[16,"before-request-hooks-for-websockets"]],"Blueprints \u2192 Route Groups":[[12,"blueprints-route-groups"]],"Building Frontend Assets":[[3,"building-frontend-assets"]],"Building a REST API":[[14,null]],"Built-in Middleware":[[13,"built-in-middleware"]],"CORS":[[10,"cors"]],"CRUD Endpoints":[[15,"crud-endpoints"]],"Changed":[[2,"changed"],[2,"id2"],[2,"id9"],[2,"id12"],[2,"id15"],[2,"id18"],[2,"id21"],[2,"id26"],[2,"id27"],[2,"id28"],[2,"id30"],[2,"id31"],[2,"id33"],[2,"id34"],[2,"id40"],[2,"id44"],[2,"id49"],[2,"id50"],[2,"id56"]],"Changelog":[[2,null]],"Chat Room":[[16,"chat-room"]],"Class-Based Views":[[10,"class-based-views"]],"Cloud Platforms":[[4,"cloud-platforms"]],"Command Line Interface":[[3,null]],"Configuration":[[5,null]],"Configuration Class Pattern":[[5,"configuration-class-pattern"]],"Cookie-Based Sessions":[[10,"cookie-based-sessions"]],"Cookies":[[10,"cookies"]],"Create a Book":[[14,"create-a-book"]],"Create a Web Service":[[7,"create-a-web-service"]],"Custom Error Handling":[[10,"custom-error-handling"]],"Custom Exception for Auth Errors":[[11,"custom-exception-for-auth-errors"]],"Custom Instance Names":[[3,"custom-instance-names"]],"Data Formats":[[16,"data-formats"]],"Database Setup":[[15,"database-setup"]],"Debug Mode":[[5,"debug-mode"]],"Define Your Models":[[14,"define-your-models"],[15,"define-your-models"]],"Delete a Book":[[14,"delete-a-book"]],"Deployment":[[4,null]],"Deprecated":[[2,"deprecated"]],"Development Sandbox":[[8,null]],"Docker":[[4,"docker"]],"Echo Server":[[16,"echo-server"]],"Environment Variables":[[5,"environment-variables"]],"Error Handling":[[14,"error-handling"]],"Feature Tour":[[10,null]],"Fixed":[[2,"fixed"],[2,"id3"],[2,"id5"],[2,"id6"],[2,"id7"],[2,"id8"],[2,"id13"],[2,"id16"],[2,"id19"],[2,"id20"],[2,"id23"],[2,"id24"],[2,"id25"],[2,"id32"],[2,"id41"],[2,"id46"],[2,"id47"],[2,"id54"],[2,"id55"]],"Future Ideas":[[1,"future-ideas"]],"Get a Single Book":[[14,"get-a-single-book"]],"Getting Started":[[9,"getting-started"]],"Gradual Migration":[[12,"gradual-migration"]],"GraphQL":[[10,"graphql"]],"HSTS":[[10,"hsts"]],"HTML Client":[[16,"html-client"]],"Hello World":[[7,"hello-world"]],"Hooks vs. Middleware":[[13,"hooks-vs-middleware"]],"How WebSockets Work":[[16,"how-websockets-work"]],"In-Memory Storage":[[14,"in-memory-storage"]],"Installation":[[6,"installation"],[15,"installation"]],"JSON APIs":[[12,"json-apis"]],"Launching from a File":[[3,"launching-from-a-file"]],"Launching from a Module":[[3,"launching-from-a-module"]],"Launching from a URL":[[3,"launching-from-a-url"]],"Lifespan Events":[[10,"lifespan-events"]],"Lifespan for Startup and Shutdown":[[15,"lifespan-for-startup-and-shutdown"]],"List All Books":[[14,"list-all-books"]],"MessagePack":[[10,"messagepack"]],"Method Filtering":[[10,"method-filtering"]],"Middleware Order":[[13,"middleware-order"]],"Migrating from Flask":[[12,null]],"Mounting Other Apps":[[10,"mounting-other-apps"]],"OpenAPI Documentation":[[10,"openapi-documentation"]],"Operations":[[8,"operations"]],"Project":[[6,null]],"Project Setup":[[14,"project-setup"]],"Putting It All Together":[[5,"putting-it-all-together"],[7,"putting-it-all-together"]],"Query Dict":[[0,"query-dict"]],"Quick Reference":[[12,"quick-reference"]],"Quick Start":[[7,null]],"Rate Limiter":[[0,"rate-limiter"]],"Rate Limiting":[[10,"rate-limiting"]],"Reading Requests":[[7,"reading-requests"]],"Removed":[[2,"removed"]],"Rendering Templates":[[7,"rendering-templates"]],"Request":[[0,"request"]],"Request ID":[[10,"request-id"]],"Responder":[[6,null]],"Response":[[0,"response"]],"Reverse Proxy":[[4,"reverse-proxy"]],"Route Groups":[[0,"route-groups"],[10,"route-groups"]],"Route Parameters":[[7,"route-parameters"],[12,"route-parameters"]],"Run It":[[14,"run-it"],[15,"run-it"]],"Run the Server":[[7,"run-the-server"]],"Running Locally":[[4,"running-locally"]],"Secret Key":[[5,"secret-key"]],"Sending Responses":[[7,"sending-responses"]],"Server-Sent Events (SSE)":[[10,"server-sent-events-sse"]],"Serving Files":[[10,"serving-files"]],"Setup":[[8,"setup"]],"Skipping Auth for Public Routes":[[11,"skipping-auth-for-public-routes"]],"Static Files":[[10,"static-files"]],"Status Code Helpers":[[0,"status-code-helpers"]],"Templates":[[12,"templates"]],"Testing":[[9,null]],"Testing Before and After Hooks":[[9,"testing-before-and-after-hooks"]],"Testing Error Handling":[[9,"testing-error-handling"]],"Testing File Uploads":[[9,"testing-file-uploads"]],"Testing Headers and Cookies":[[9,"testing-headers-and-cookies"]],"Testing JSON APIs":[[9,"testing-json-apis"]],"Testing Lifespan Events":[[9,"testing-lifespan-events"]],"Testing Request Validation":[[9,"testing-request-validation"]],"Testing WebSockets":[[9,"testing-websockets"],[16,"testing-websockets"]],"The API Class":[[0,"the-api-class"]],"The Big Differences":[[12,"the-big-differences"]],"The Idea":[[6,"the-idea"]],"Tips":[[9,"tips"],[15,"tips"]],"Trusted Hosts":[[10,"trusted-hosts"]],"Try It Out":[[14,"try-it-out"]],"Tutorials":[[6,null]],"Type Convertors":[[7,"type-convertors"]],"Unreleased":[[2,"unreleased"]],"Update a Book":[[14,"update-a-book"]],"User Guide":[[6,null]],"Using .env Files":[[5,"using-env-files"]],"Using Fixtures":[[9,"using-fixtures"]],"Using PostgreSQL":[[15,"using-postgresql"]],"Using SQLAlchemy":[[15,null]],"Using Sessions for Web Apps":[[11,"using-sessions-for-web-apps"]],"Using Starlette Middleware":[[13,"using-starlette-middleware"]],"Uvicorn Directly":[[4,"uvicorn-directly"]],"WebSocket Support":[[10,"websocket-support"]],"WebSocket Tutorial":[[16,null]],"What You Get":[[6,"what-you-get"]],"What\u2019s Next":[[14,"what-s-next"]],"When to Use What":[[13,"when-to-use-what"]],"Writing Middleware":[[13,null]],"[v3.2.0] - 2026-03-22":[[2,"v3-2-0-2026-03-22"]],"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","guide-config","index","quickstart","sandbox","testing","tour","tutorial-auth","tutorial-flask","tutorial-middleware","tutorial-rest","tutorial-sqlalchemy","tutorial-websockets"],"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","guide-config.rst","index.rst","quickstart.rst","sandbox.md","testing.rst","tour.rst","tutorial-auth.rst","tutorial-flask.rst","tutorial-middleware.rst","tutorial-rest.rst","tutorial-sqlalchemy.rst","tutorial-websockets.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_middleware() (responder.api method)":[[0,"responder.API.add_middleware",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]],"backgroundqueue (class in responder.background)":[[0,"responder.background.BackgroundQueue",false]],"before_request() (responder.api method)":[[0,"responder.API.before_request",false]],"check() (responder.ext.ratelimit.ratelimiter method)":[[0,"responder.ext.ratelimit.RateLimiter.check",false]],"client (responder.request property)":[[0,"responder.Request.client",false]],"content (responder.request property)":[[0,"responder.Request.content",false]],"cookies (responder.request property)":[[0,"responder.Request.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]],"full_url (responder.request property)":[[0,"responder.Request.full_url",false]],"get() (responder.models.querydict method)":[[0,"responder.models.QueryDict.get",false]],"get_list() (responder.models.querydict method)":[[0,"responder.models.QueryDict.get_list",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]],"install() (responder.ext.ratelimit.ratelimiter method)":[[0,"responder.ext.ratelimit.RateLimiter.install",false]],"is_100() (in module responder.status_codes)":[[0,"responder.status_codes.is_100",false]],"is_200() (in module responder.status_codes)":[[0,"responder.status_codes.is_200",false]],"is_300() (in module responder.status_codes)":[[0,"responder.status_codes.is_300",false]],"is_400() (in module responder.status_codes)":[[0,"responder.status_codes.is_400",false]],"is_500() (in module responder.status_codes)":[[0,"responder.status_codes.is_500",false]],"is_json (responder.request property)":[[0,"responder.Request.is_json",false]],"is_secure (responder.request property)":[[0,"responder.Request.is_secure",false]],"items() (responder.models.querydict method)":[[0,"responder.models.QueryDict.items",false]],"items_list() (responder.models.querydict method)":[[0,"responder.models.QueryDict.items_list",false]],"media() (responder.request method)":[[0,"responder.Request.media",false]],"method (responder.request property)":[[0,"responder.Request.method",false]],"mimetype (responder.request property)":[[0,"responder.Request.mimetype",false]],"module":[[0,"module-responder",false]],"mount() (responder.api method)":[[0,"responder.API.mount",false]],"ok (responder.response property)":[[0,"responder.Response.ok",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]],"querydict (class in responder.models)":[[0,"responder.models.QueryDict",false]],"ratelimiter (class in responder.ext.ratelimit)":[[0,"responder.ext.ratelimit.RateLimiter",false]],"redirect() (responder.api method)":[[0,"responder.API.redirect",false]],"redirect() (responder.response method)":[[0,"responder.Response.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]],"routegroup (class in responder.api)":[[0,"responder.api.RouteGroup",false]],"run() (responder.api method)":[[0,"responder.API.run",false]],"run() (responder.background.backgroundqueue method)":[[0,"responder.background.BackgroundQueue.run",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.api method)":[[0,"responder.API.session",false]],"set_cookie() (responder.response method)":[[0,"responder.Response.set_cookie",false]],"sse() (responder.response method)":[[0,"responder.Response.sse",false]],"state (responder.request property)":[[0,"responder.Request.state",false]],"static_app (responder.api property)":[[0,"responder.API.static_app",false]],"status_code_safe (responder.response property)":[[0,"responder.Response.status_code_safe",false]],"stream() (responder.response method)":[[0,"responder.Response.stream",false]],"stream_file() (responder.response method)":[[0,"responder.Response.stream_file",false]],"task() (responder.background.backgroundqueue method)":[[0,"responder.background.BackgroundQueue.task",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_middleware"],[0,2,1,"","add_route"],[0,2,1,"","after_request"],[0,2,1,"","before_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,"","run"],[0,2,1,"","schema"],[0,2,1,"","serve"],[0,2,1,"","session"],[0,3,1,"","static_app"],[0,2,1,"","template"],[0,2,1,"","template_string"],[0,2,1,"","url_for"]],"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,3,1,"","is_secure"],[0,2,1,"","media"],[0,3,1,"","method"],[0,3,1,"","mimetype"],[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,2,1,"","file"],[0,3,1,"","ok"],[0,2,1,"","redirect"],[0,2,1,"","set_cookie"],[0,2,1,"","sse"],[0,3,1,"","status_code_safe"],[0,2,1,"","stream"],[0,2,1,"","stream_file"]],"responder.api":[[0,1,1,"","RouteGroup"]],"responder.background":[[0,1,1,"","BackgroundQueue"]],"responder.background.BackgroundQueue":[[0,2,1,"","run"],[0,2,1,"","task"]],"responder.ext.ratelimit":[[0,1,1,"","RateLimiter"]],"responder.ext.ratelimit.RateLimiter":[[0,2,1,"","check"],[0,2,1,"","install"]],"responder.models":[[0,1,1,"","QueryDict"]],"responder.models.QueryDict":[[0,2,1,"","get"],[0,2,1,"","get_list"],[0,2,1,"","items"],[0,2,1,"","items_list"]],"responder.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"]]},"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"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:method","3":"py:property","4":"py:function"},"terms":{"":[0,2,3,4,5,6,7,9,10,11,12,13,15,16],"0":[3,4,5,7,10,14,15],"00":0,"01":0,"0441172719":14,"1":[0,3,4,5,7,10,11,12,14,16],"10":[0,6,7,10],"100":[0,2,10],"127":[3,4,5],"13":4,"150mb":4,"1965":14,"2":[4,7,9,10,14,15,16],"200":[0,7,9,10,14],"201":[7,9,12,14,15],"2026":0,"204":[14,15],"24":11,"2xx":0,"3":[4,6,7,9,10,14,16],"3000":5,"301":[0,7],"308":2,"32":5,"3600":[0,10],"3f":13,"4":[4,7],"40":2,"400":[0,9,10,14],"401":[0,10,11],"404":[0,7,12,14,15],"41d4":7,"422":[2,9,10,14],"429":[0,10],"446655440000":7,"500":[2,7,9,10,13,14],"5000":10,"5042":[3,4,7,11,14,16],"550e8400":7,"561":2,"60":[0,2,10],"600":[0,10],"7":11,"8":0,"80":4,"8000":[4,7],"8192":0,"9":9,"978":14,"99":9,"A":[0,4,5,6,7,10,13,16],"And":[5,6,7],"As":[0,10],"But":[7,9,10],"By":[3,7,9,10,13,14],"For":[0,3,4,5,7,9,10,11,14,15],"If":[0,3,5,6,7,9,10,12,15],"In":[0,5,6,7,10,11,12,15,16],"It":[0,3,6,9,10,13],"No":[6,9,10,12,14],"Not":7,"On":[5,10],"One":[6,7,9],"Or":[0,5],"That":[4,6,7,9],"The":[2,3,4,5,7,9,10,11,13,14,15,16],"There":[9,10],"These":7,"To":[14,15],"Will":0,"With":[0,5],"__init__":11,"__main__":[4,6,7,9,14,15],"__name__":[4,6,7,9,10,12,14,15],"__tablename__":15,"_route_for":2,"a2wsgi":2,"a716":7,"abc":14,"abc123":[0,9,10,11],"abil":2,"abl":0,"abort":12,"about":[4,7,9,12],"abov":[6,11],"abstract":15,"accept":[0,2,7,9,10,16],"access":[0,4,10,11,15],"acm":3,"acquir":8,"across":[4,10,15],"activ":[8,16],"actual":[0,7,10],"ad":[0,1,6,10],"adapt":10,"add":[0,1,2,5,7,11,13,14,15,16],"add_event_handl":0,"add_head":[10,13],"add_middlewar":[0,13],"add_request_id":0,"add_rout":[0,10],"add_tim":[9,10],"add_vers":9,"addeventlisten":16,"addit":[0,13],"address":[0,5,7,10],"adher":2,"admin":11,"affect":10,"after":[0,6,7,11,15,16],"after_request":[0,1,2,9,10,13],"ag":[0,10],"again":11,"against":[0,4,10,11],"agre":7,"ai":10,"aiosqlit":15,"alemb":15,"algorithm":11,"alias":2,"alic":10,"all":[0,2,3,4,6,8,9,10,12,13,15,16],"alloc":9,"allow":[6,10],"allow_credenti":[0,10],"allow_head":[0,10],"allow_method":[0,5,10],"allow_origin":[0,5,10],"allow_origin_regex":0,"allowed_host":[0,5,10],"alongsid":10,"alpin":4,"alreadi":[0,9,10],"also":[0,2,3,7,9,10],"altern":[0,10],"alwai":[4,5,6,7,10],"an":[0,3,5,7,10,12,13,14,15,16],"analysi":10,"angle_bracket":12,"angular":10,"ani":[0,3,4,5,6,7,9,10,11,13,14,15,16],"annot":[6,7,10],"annual":[7,10],"answer":[10,16],"anyon":6,"anyth":7,"anywher":[4,10],"api":[2,3,4,5,6,7,10,13,15,16],"api_kei":11,"api_v1":12,"apispec":2,"app":[0,2,3,4,5,6,7,9,12,14,15,16],"appar":0,"apparent_encod":0,"appear":10,"appendchild":16,"appli":[0,13],"applic":[0,2,3,4,5,7,9,10,11,13,14,15],"approach":[5,10,11,12],"appropri":[10,15],"ar":[0,2,3,4,6,7,9,10,11,12,13,15,16],"architectur":10,"aren":[7,13],"arg":[0,12],"argument":[0,2,7,12,13],"around":[0,6],"arriv":[7,12],"asgi":[0,2,4,6,7,10,12,13],"ask":[6,10,16],"assert":[9,16],"asset":[0,4,6,10],"assign":14,"associ":0,"async":[0,1,2,4,6,7,9,10,11,12,13,14,15,16],"async_sess":15,"async_sessionmak":15,"asynccontextmanag":[10,15],"asynchron":[2,4],"asyncio":15,"asyncpg":15,"attach":10,"attack":[4,5,10],"attribut":[0,3,15],"audio":16,"auth":[6,10,13],"auth_check":10,"auth_guard":11,"authent":[6,7,10,13,16],"autherror":11,"author":[0,7,8,10,11,13,14,15],"auto":[2,10,16],"auto_escap":0,"autobuild":8,"autoincr":15,"automat":[0,2,4,5,6,7,9,10,11,12,13,14,15],"avail":[0,7],"avoid":[9,10],"aw":4,"await":[0,2,7,9,10,11,12,13,14,15,16],"azur":[3,4],"b":[7,9,13,15,16],"back":[0,7,10,15,16],"background":[2,6],"backgroundqueu":0,"backlog":6,"bad":[9,10,14],"balanc":4,"bandwidth":10,"bar":10,"base":[0,2,6,11,13,15],"base_url":0,"basehttpmiddlewar":13,"basemodel":[0,9,10,14,15],"basic":7,"batteri":6,"battl":6,"bearer":6,"becaus":[0,6,7,11,12,14],"becom":[0,15],"been":0,"befor":[0,2,6,7,11,13,15],"before_request":[0,1,2,9,10,11,12,13,16],"begin":15,"behavior":[5,13],"behind":[4,10],"being":0,"best":6,"better":6,"between":13,"bidirect":[10,16],"big":6,"biggest":12,"bin":8,"binari":[7,10,16],"bind":[0,4,5,7],"blob":3,"block":[0,7,9,10,12,16],"blueprint":6,"bodi":[0,2,7,9,10,12,14,16],"boilerpl":6,"book":[6,15],"book_id":[14,15],"bookin":[14,15],"bookout":15,"books_db":14,"bool":5,"born":6,"both":[6,7,10,12,16],"bottom":14,"box":13,"bp":12,"break":9,"bring":6,"broadcast":16,"broke":[7,9],"broker":7,"browser":[7,8,10,11,14,16],"bucket":[0,10],"buffer":[0,7],"bufix":2,"bug":[2,9],"bugfix":2,"build":[2,4,6,7,8,10,11,15,16],"builder":15,"built":[2,4,6,7,9,10,16],"bump":2,"bypass":4,"byte":[0,2,6,7,13,16],"c":[5,16],"cach":[7,9,10],"caddi":4,"call":[3,7,9,10,13,14,16],"call_next":13,"callabl":0,"came":7,"can":[0,2,3,4,5,6,7,9,10,11,12,13,14,15,16],"carri":7,"case":[0,6,7,13],"cat":10,"catalog":14,"catch":[10,11,13],"categori":0,"cd":8,"ceas":2,"celeri":7,"cell":10,"central":[0,7],"certain":0,"certif":4,"chang":[0,15],"changelog":6,"channel":[10,16],"chardet":0,"chat":[6,10],"check":[0,8,9,10,11,13,16],"check_api_kei":11,"check_auth":0,"check_exist":[0,10,14,15],"choic":[7,10],"choos":10,"chunk":[0,10],"chunk_siz":0,"ci":2,"circuit":[0,1,2,6,10],"class":[2,6,7,9,11,13,14,15],"clean":[6,10,14],"cleaner":[10,11],"cleanli":5,"cleanup":10,"clear":11,"cli":2,"click":6,"client":[0,2,6,7,9,10,11,14],"clone":8,"close":[9,10,15,16],"close_database_connection_pool":0,"cloud":[3,5,6,7],"cmd":4,"code":[2,3,4,5,6,7,8,9,10,11,14,15,16],"collabor":[10,16],"colon":3,"column":15,"com":[0,3,5,7,8,10],"combin":7,"come":[7,10,12],"command":[4,6],"commit":[5,15],"common":[3,4,5,7,10,11,12,13,14],"commonli":7,"commun":[10,11],"compact":10,"compat":[0,2],"complement":[1,10],"complet":[7,10,14,16],"complex":[13,15],"compon":13,"compos":15,"compress":[6,7,13],"compromis":5,"comput":7,"concept":[2,10,12],"concern":[7,13],"concurr":[0,4],"condit":[9,10],"config":[5,15],"configur":[0,2,4,6,7,9,10,13],"conflict":2,"confus":9,"conn":15,"connect":[0,4,7,9,10,15,16],"consid":[1,7,15],"consist":[7,10],"consol":[10,15],"const":[10,16],"constructor":[0,13],"consum":10,"contact":0,"contain":[0,4,7],"container":4,"content":[0,2,6,7,9,10,14],"content_typ":[0,9,10],"context":[2,6,9,10,15,16],"contextlib":[10,15],"continu":7,"contract":9,"control":[0,4,5,7,10,11,15],"conveni":[0,5,15],"convent":[4,14],"convert":[2,7],"convertor":[2,6,12],"cooki":[0,2,5,6,7,11,12,13],"copi":4,"cor":[0,2,5,6,9,13],"core":[2,4],"coroutin":0,"correctli":4,"correspond":15,"cors_en":5,"cors_origin":5,"cors_param":[0,5,10],"corsmiddlewar":13,"count":2,"cover":[5,11,13],"cpu":[0,4,7],"crash":2,"creat":[0,6,8,9,10,11,12,15],"create_al":15,"create_asgi_app":10,"create_async_engin":15,"create_book":[14,15],"create_connect":16,"create_item":[0,10,12],"create_pet":10,"create_token":11,"createel":16,"creation":15,"credenti":11,"cross":[10,13],"crud":[6,7,14],"css":10,"csv":10,"curl":[3,7,11,14],"curly_brac":12,"custom":[0,2,6,7,9,14,16],"cut":13,"cycl":7,"d":[7,9,14],"dashboard":[5,10,11,16],"data":[0,2,5,6,7,9,10,11,12,14,15],"databas":[5,6,7,9,10,11,14],"database_url":[5,15],"date":0,"datetim":11,"db":[5,15],"debug":[0,2,6,10,12],"decim":7,"declar":[2,7],"declarativebas":15,"decod":[2,10,11],"decor":[0,2,7,10],"deep":6,"def":[0,6,7,9,10,11,12,13,14,15,16],"def456":11,"default":[0,1,2,3,5,7,9,10,15],"default_valu":[0,10],"defend":10,"defin":[0,6,7,10,11],"definit":0,"del":14,"delet":[5,6,7,10,15],"delete_book":[14,15],"deliber":7,"demo":3,"denpend":2,"dep":2,"depend":[2,4,9,13],"deploi":[4,6],"deploy":6,"depth":10,"describ":10,"descript":[0,10],"design":[7,10,14],"detail":[4,5,10,14],"detect":[0,6,10],"dev":[2,5],"develop":[3,4,5,10,15],"devic":10,"dict":[6,7,10,11,14,15],"dictionari":0,"differ":[0,3,5,6,9,10,15],"digit":7,"direct":[0,2,10],"directli":[3,6,7,9,14,15],"directori":[0,7,10],"disabl":[9,15],"discard":16,"disconnect":16,"disk":[0,7,10],"dispatch":[2,10,13],"dispos":15,"distribut":[4,10],"div":16,"django":[6,10],"do":[4,5,7,10],"doc":[8,9,10,11,14],"docker":[5,6],"dockerfil":4,"docs_rout":[0,10,14],"docstr":10,"doctyp":16,"document":[0,2,4,6,8,14,16],"doe":15,"doesn":[0,10,14,15],"domain":[0,5,10],"don":[5,7,9,10,11,12,15],"done":4,"dot":3,"dotenv":5,"doubl":14,"down":[10,15],"download":10,"dramat":2,"driver":15,"drop":[2,16],"dump":2,"dune":14,"duplex":10,"duplic":10,"durat":13,"dure":[3,4,9,15],"duti":14,"dynam":7,"e":[0,16],"e29b":7,"each":[0,4,9,10,15],"easi":[3,5,7],"echo":[6,7,15],"ecosystem":[6,7],"edit":8,"editor":[10,16],"effect":[9,14],"effici":4,"either":[0,16],"element":[0,10],"els":[7,10,11,12,15],"email":7,"emb":10,"empti":[0,14],"enabl":[2,10,13,14],"enable_async":7,"enable_hst":[0,10,13],"encod":[0,2,11],"encrypt":11,"end":[7,14],"endpoint":[0,2,6,9,10,11,12,14],"enforc":10,"engin":[7,15],"enough":[4,13],"ensur":[10,14,15,16],"enter":[9,16],"entir":[0,10,11,13,14],"entri":14,"env":[4,6],"environ":[0,3,4,6,7],"eol":2,"equival":12,"error":[0,2,5,6,15],"errorhandl":12,"escap":0,"essenti":10,"establish":16,"etc":[7,9,10],"even":[3,4,10,16],"event":[0,2,6,7,12,16],"event_typ":0,"eventsourc":10,"ever":[5,6],"everi":[0,4,5,6,7,9,10,11,13,14,15],"everyth":[0,2,4,7,10,12,14,15,16],"everywher":[3,5],"evolv":15,"exactli":[9,10],"exampl":[0,3,5,7,10,11],"exc":[0,9,10,11,14],"exceed":[0,10],"except":[0,2,6,9,10,13,16],"exception_cl":0,"exception_handl":[0,2,9,10,11,12,14],"exceptionmiddlewar":13,"execut":15,"exist":[0,2,6,12,13],"exit":[9,10,16],"exp":11,"expect":7,"expir":[0,10,11],"expire_on_commit":15,"explain":10,"explan":0,"explicit":[7,10,12],"explicitli":[10,12],"explor":[1,7,10],"export":[5,10],"expos":[4,5],"expose_head":0,"express":[6,7],"ext":[0,2,10,15],"extens":[2,7],"extern":7,"extra":[2,4,10],"extract":0,"extrem":7,"f":[0,6,7,9,10,11,13,14,16],"factori":15,"fail":[9,10,15],"failur":[2,9],"falcon":[6,10],"fall":0,"fals":[0,5,9,10,14,15],"familiar":[6,12],"fast":[4,6,7,9],"fastapi":6,"faster":10,"featur":[0,6,7],"feed":10,"feel":6,"fetch":3,"few":12,"fido":10,"field":[0,2,9,10,14],"figur":7,"file":[0,2,4,6,7,14,15,16],"filenam":[0,9],"filepath":7,"filesystem":[2,3],"fill":7,"filter":[6,7],"final":16,"find":[3,10,12],"fine":[4,10,15],"fire":[0,9],"first":[0,10,11,13,14,15],"fit":6,"fix":8,"fixtur":6,"flag":[4,10,15],"flask":[6,10],"flask_app":[10,12],"flatten":2,"float":[0,7,9],"flow":[11,13],"fly":4,"follow":7,"font":10,"forg":[5,10],"forget":0,"form":[0,7,11,12],"format":[0,2,6,7,8,10,11],"formerli":10,"forward":[4,10],"found":[2,7,14,15],"foundat":10,"fragment":3,"framework":[6,10],"franca":7,"frank":14,"freeli":16,"fresh":9,"from":[0,2,4,5,6,7,9,10,11,13,14,15,16],"from_attribut":15,"front":4,"frontend":[6,10],"fsspec":3,"full":[0,2,3,7,10],"full_url":[0,7,10],"fulli":7,"fun":6,"func":0,"function":[0,7,10,12,13],"futur":[0,6],"g":0,"game":[10,16],"gatewai":4,"gener":[0,2,5,6,9,10,13,14],"get":[0,2,5,7,10,11,15],"get_book":[14,15],"get_list":0,"get_m":11,"get_us":[0,10,12],"getelementbyid":16,"getenv":5,"git":[5,8],"github":[3,8],"gitignor":5,"give":[0,9,10,13,15],"given":[0,9],"global":[12,14],"gmt":0,"go":[7,12],"goe":[10,12],"good":[4,6,10],"googl":[3,4],"grace":4,"grade":7,"gradual":[6,10],"graduat":13,"grain":15,"graphen":[0,2,6,10],"graphiql":[2,6,10],"graphql":[0,2,6],"great":[3,6,10],"greet":[6,7,10],"greet_world":6,"greetingresourc":10,"group":[2,6],"grow":10,"guard":[6,11],"guid":[5,7,11,12,15],"guido":7,"gzip":[6,7],"gzipmiddlewar":13,"h":[7,11,14],"h1":[7,11],"ha":[7,9,12,14,16],"half":7,"halv":10,"hand":0,"handl":[0,2,4,5,6,7,11,16],"handle_auth_error":11,"handle_value_error":[0,10,14],"handler":[0,6,9,10,11,13,14],"handshak":16,"happen":[6,7],"hard":9,"hasn":0,"have":[0,2,3,7,9,12,13,14],"hdf":3,"head":3,"header":[0,2,4,5,6,7,10,11,12,13,16],"health":11,"heavi":7,"hello":[0,3,6,9,10,12,16],"hello_html":7,"hello_json":7,"hello_to":7,"hello_world":7,"helloworld":3,"help":[0,15],"helper":[6,11],"herbert":14,"here":[4,5,7,10,12,16],"high":[4,10],"hold":[0,6,7],"home":6,"honor":[4,7],"hood":10,"hook":[0,1,2,6,7,11],"host":[0,2,4,6,7,13],"hostnam":10,"hour":[10,11],"how":[5,6,7,10,11,12,14,15],"hs256":11,"hst":6,"html":[0,2,6,7,10,11,12],"http":[0,2,3,4,5,6,7,8,9,10,11,14,16],"httponli":[0,10],"httpsredirect":0,"httpsredirectmiddlewar":[0,4,13],"httpx":[2,9],"i":[0,2,3,4,5,6,7,9,10,11,12,13,14,15,16],"id":[0,2,6,7,12,13,14,15,16],"idea":12,"idempot":14,"ident":12,"identifi":10,"imag":[4,7,10,16],"immedi":[0,6,7,10],"immut":2,"imperson":5,"implement":9,"import":[0,2,3,5,6,7,9,10,11,12,13,14,15,16],"improv":2,"inaccess":0,"includ":[3,4,6,7,8,9,10,11,14],"incom":[0,7,14],"incredibli":10,"independ":4,"index":[0,7,10,16],"individu":10,"industri":10,"info":[0,2,10],"inform":[0,14],"infrastructur":[4,6],"inherit":[0,10],"initi":[0,9],"inject":10,"innermost":13,"input":[2,9,10,14,16],"insensit":[0,6,7],"insid":13,"inspect":0,"instal":[0,2,3,4,5,8,10,11,16],"instanc":[0,4,6,7,9],"instant":7,"instead":[0,7,9,10,13,14,15,16],"int":[0,2,7,10,11,12,14,15],"integ":[0,7,14,15],"integr":[13,14,15],"intens":7,"intent":6,"intention":10,"interact":[2,10,14],"interfac":[4,6,7,12],"intern":[9,10,13],"invalid":[5,9,10,11],"invalidtokenerror":11,"involv":0,"io":4,"iot":10,"ip":[0,7,10],"is_100":0,"is_200":0,"is_300":0,"is_400":0,"is_500":0,"is_class_bas":2,"is_json":[0,2,7],"is_secur":[0,7],"isbn":[14,15],"isn":[7,10,12],"issu":[2,11],"item":[0,9,10,12,15],"itemin":0,"itemout":0,"items_list":0,"iter":16,"its":[0,2,4,14,15],"itself":11,"j":10,"jan":0,"javascript":[0,3,10],"jinja2":[0,7,12],"jpeg":10,"jpg":10,"json":[0,3,6,7,10,11,14,16],"jsonifi":12,"jump":7,"just":[4,9,10,13,15,16],"jwt":11,"keep":[2,5,7,9,10,13,15,16],"kei":[0,6,7,9,10,12,15,16],"kennethreitz":[3,8],"keypress":16,"keyword":[0,7,12,13],"know":[5,7],"known":[0,11],"kwarg":0,"languag":10,"larg":[0,2,10],"larger":[5,9,13],"last":[0,13],"later":[7,9],"launch":[4,6],"layer":13,"lazi":2,"lazili":0,"lead":9,"learn":10,"legaci":12,"less":10,"let":[3,4,7,10,14,15],"level":[4,13],"librari":[9,10,16],"licens":0,"lifecycl":16,"lifespan":[0,2,6,14],"lift":7,"lightn":4,"lightweight":7,"like":[4,7,9,10,11,13,14,16],"limit":[1,2,4,6,13,15,16],"line":[6,7],"liner":2,"lingua":7,"list":[0,6,7,9,10,15],"list_book":[14,15],"list_item":10,"list_pet":10,"list_us":[0,10,12],"list_users_v2":10,"live":[10,16],"ll":[6,7,10,12,14,15],"load":[0,2,3,4,5,7,10],"load_dotenv":5,"local":[3,5,6,12],"localhost":[5,7,11,14,15,16],"locat":[0,7,11,12],"log":[4,10,11,13],"log_level":2,"log_request":13,"log_respons":10,"logic":[6,7,10],"login":[10,11],"logout":11,"long":7,"look":[3,6,7],"loop":[7,12,16],"lose":[2,15],"lower":[0,5],"lowercas":7,"machin":10,"made":[0,6],"magic":12,"mai":4,"main":3,"make":[4,5,7,9,10,11,12],"malici":10,"manag":[2,5,6,9,10,11,13,15,16],"mani":[0,4,7,10],"manual":0,"map":[2,7,10,12,15],"mapper":15,"marimo":10,"mark":[2,10],"marker":2,"marshmallow":[0,2,10],"match":[0,5,7,10,14],"matter":10,"max_ag":[0,10],"maximum":0,"me":11,"mean":[4,7,12],"mechan":10,"media":[0,2,6,7,9,10,11,12,14,15],"memori":[0,2,6,7,10],"messag":[0,7,10,11,16],"messagepack":[2,6],"metadata":[0,7,15],"method":[0,6,7,9,11,12,13,14,15,16],"microservic":10,"middelwar":0,"middlewar":[0,1,2,6,7,10],"middleware_cl":0,"middleware_config":0,"might":7,"migrat":[2,6,10,15],"mime":0,"mimetyp":[0,7,10],"min":[0,10],"mind":13,"minim":[4,16],"minimum":2,"minor":2,"miss":[2,9,10,11],"mix":10,"ml":10,"mode":[0,6,8],"model":[0,6,9,10],"model_dump":15,"model_valid":15,"modern":[4,10,11,12,15],"modifi":[0,10,13,14,15],"modul":[0,2,6,7,10],"more":[0,3,4,7,10,11,12,13,14,16],"most":[0,3,4,5,7,9,10,11,13,15],"mount":[0,2,6,12],"move":[2,7,10,12],"msgpack":[2,10],"much":[7,10],"multi":0,"multipart":[2,7],"multipl":[2,4,10,14],"multiplay":[10,16],"must":[0,7,10,11,14,16],"mutabl":6,"mutat":[0,7,12],"my_templ":7,"myapi":4,"myapp":3,"mydb":15,"mysql":15,"n":[0,7],"name":[0,5,6,7,9,10,12],"namespac":2,"nativ":12,"natur":[9,15],"nearli":12,"need":[4,5,6,7,9,10,11,12,13,15,16],"negoti":[0,6,7,9,10],"nervou":7,"network":9,"never":[5,10,11,14],"new":[0,7,9,10,12,14,16],"new_endpoint":12,"next":[6,13],"next_id":14,"nginx":4,"non":7,"none":[0,2,11,14,15],"normal":[10,16],"notabl":2,"notasecret":0,"note":[7,10,12,14],"notebook":10,"notic":7,"notif":10,"now":[2,7,10,11,15,16],"npm":3,"nullabl":15,"number":[0,7],"o":[0,5,7,12],"object":[0,2,7,10,12,13,15,16],"objecttyp":[0,10],"obviou":9,"off":[5,7],"offload":4,"offset":15,"often":[3,7,10],"ok":[0,7,9,14],"old":12,"on_delet":10,"on_ev":[0,9,10],"on_get":[6,10],"on_post":[6,10],"on_put":10,"on_request":[6,10],"on_startup":9,"onc":[10,12,14],"one":[0,2,5,6,7,10,12,15],"ones":[6,7,10],"onion":13,"onli":[0,5,7,10,12,14],"onmessag":[10,16],"open":[2,7,8,15],"open_database_connection_pool":0,"openapi":[0,2,6,14],"openapi_rout":0,"openapi_them":0,"oper":[7,10,12,14],"opt":10,"option":[0,2,4,6,7,13],"option1":13,"option2":13,"order":[0,6],"order_bi":15,"organ":[0,5,9,10,12],"origin":10,"orm":15,"other":[0,2,6,7,15],"otherwis":[0,10],"our":14,"out":[5,6,7,13],"outermost":13,"outgo":7,"output":14,"outsid":13,"over":[0,6,7,10,12,15],"overal":2,"overhead":9,"overrid":[0,10],"overwhelm":10,"own":[4,10,15],"p":[4,11,16],"pace":10,"packag":[2,3,4],"page":[0,2,5,7,10,11,16],"pagin":7,"painless":9,"pair":0,"param":[0,2,6,7,12],"paramet":[0,6,9,10,14],"parameter":0,"pars":[0,2,7,9,10],"parser":2,"part":[2,6,7,10],"parti":6,"pass":[0,7,9,12,13,15,16],"passion":6,"password":11,"patch":[7,10],"path":[0,2,3,6,7,9,10,11,13],"path_matches_rout":0,"path_param":[0,2,7],"pattern":[4,6,7,10,11,12,14,15],"payload":11,"pdf":[7,9,10],"pep":2,"per":[0,4,9,10],"perfect":10,"period":[0,2,10],"perman":7,"permit":10,"persist":[0,10,16],"person":6,"pet":[0,10],"petin":10,"petout":10,"petschema":[0,10],"photo":10,"piec":10,"pin":2,"pip":[2,3,4,5,6,8,11,15,16],"place":[5,10],"placehold":[7,16],"plain":[0,7,10,16],"platform":[2,5,6,7],"pleasant":6,"plot":10,"plu":15,"plugin":2,"poethepoet":2,"point":[3,4,14],"polici":10,"pool":[0,6,7,10,12,15],"popular":[7,10,15],"port":[0,4,5,7,9],"portion":0,"possibl":7,"post":[0,5,7,9,10,11,12,14,15],"postgresql":6,"potenti":2,"power":[6,9,10,13,15,16],"practic":10,"prefer":10,"prefix":[0,2,10],"present":7,"prevent":[5,10],"price":[0,9],"primari":0,"primary_kei":15,"print":[0,5,10,13,15,16],"privat":10,"proce":16,"process":[4,6,7,9,10,13],"process_data":7,"prod":5,"produc":[0,7],"product":[2,3,4,5,6,7,9,10,11,15],"profil":10,"programmat":10,"progress":10,"project":[2,3,8],"propag":9,"proper":[7,11,15],"properli":[5,9,15],"properti":[0,2,7,9],"protect":[4,11],"proto":4,"protocol":[3,7,9,10,16],"prototyp":[2,3,6],"provid":0,"proxi":6,"public":[0,6,10],"public_path":11,"push":[4,6,10],"put":[6,10,14,15],"py":[2,3,4,5,7,9,10,14,15],"pydant":[0,2,9,10,14,15],"pyjwt":11,"pyproject":2,"pytest":[6,8,9],"python":[0,2,3,4,5,6,7,10,14,15,16],"pythonpath":3,"q":[7,12],"queri":[2,6,7,10,15,16],"query_str":0,"querydict":0,"queue":[6,7],"quick":[0,3,6],"r":[7,9],"race":9,"railwai":4,"rais":[0,2,9,10,11],"raise_server_except":9,"random":[0,5],"rang":[0,7,10],"rapidoc":[0,10],"rare":7,"rate":[1,2,4,6,13],"rate_limit":0,"ratelimit":[0,2,10,13],"rather":9,"raw":[0,3,6,7,10,13,15,16],"re":[0,4,7,9,10,12,13,14,16],"reach":[11,13],"react":[2,10],"reactiv":10,"read":[0,2,5,6,10,11,12,14,15],"readabl":10,"readi":[4,5,6,7],"real":[2,5,10,11,14,15],"realli":[9,10],"receiv":[0,6,7,9,10,11,13,16],"receive_byt":[10,16],"receive_incom":7,"receive_json":[10,16],"receive_text":[9,10,16],"recommend":10,"reconnect":10,"recv":16,"redirect":[0,2,7,11,12],"redoc":[0,10],"reduc":2,"ref":3,"refactor":[2,9],"refer":[6,9],"refresh":15,"refus":10,"regist":[0,9,10,11],"register_blueprint":12,"reject":[9,10],"relat":[0,10,15],"releas":8,"reliabl":9,"remain":10,"rememb":11,"remov":[10,12,14,16],"renam":9,"render":[0,1,4,6,10],"render_async":7,"render_templ":12,"replac":[2,7,10,14],"report":[7,9,10],"repres":0,"represent":0,"req":[0,2,6,7,9,10,11,12,13,14,15,16],"request":[2,3,4,6,11,12,13,14,15],"request_id":[0,2,10,13],"request_model":[0,2,9,10,14,15],"requir":[0,2,9,10],"research":6,"resolv":[2,10],"resolve_hello":[0,10],"resourc":[7,10,14],"resp":[0,2,6,7,9,10,11,12,13,14,15,16],"respect":3,"respond":[0,2,3,4,5,7,8,9,10,11,12,13,14,15,16],"respons":[2,6,9,10,12,13,14,15,16],"response_model":[0,2,10,14,15],"rest":[4,6,7,10],"restrict":10,"result":[0,7,10,15],"resum":2,"resume_incomplet":2,"retri":[0,10],"retriev":[0,10,14],"return":[0,2,6,7,9,10,11,12,13,14,15],"reusabl":11,"revers":[0,6],"rfc3986":2,"right":[6,7,10,12,13],"roll":15,"room":6,"root":10,"rotat":5,"rout":[2,6,9,13,14,15,16],"routegroup":0,"router":2,"ruff":8,"rule":5,"run":[0,2,3,6,8,9,10,12,13,16],"run_sync":15,"runner":[2,4],"runtimeerror":0,"s3":3,"safe":[2,14],"sai":7,"said":4,"same":[3,4,6,7,10,12,14,15],"sandbox":6,"save":[7,16],"scalar":15,"scale":6,"schema":[0,2,6,10,11,14,15],"scope":[0,10,13],"scratch":[6,14],"script":16,"search":[0,2,7,10],"second":0,"secret":[6,10,11],"secret_kei":[0,5,10,11],"section":10,"secur":[0,10,13],"see":[0,4,5,7,9,10,13,14,15],"select":[0,2,15],"self":[0,10,11,13],"semant":2,"send":[0,2,6,9,10,11,14,16],"send_byt":[10,16],"send_email":0,"send_json":[10,16],"send_text":[9,10,16],"sensit":11,"sent":[0,2,6,7,13],"separ":[7,9,14],"sequenti":2,"serial":[0,2,7,10,16],"serializ":7,"serv":[0,2,3,4,5,6,7,9,14,16],"server":[0,2,3,4,5,6,9,11,13,14,15],"servererrormiddlewar":13,"servestat":2,"servic":[0,3,6,9,10,14],"session":[0,2,5,6,7,9,12,13,15],"session_id":10,"sessionmiddlewar":13,"set":[0,2,4,5,6,7,8,9,10,11,15,16],"set_cooki":[0,2,10],"set_text":0,"setattr":15,"setup":[2,5,6,9],"sever":[10,13],"sftp":3,"share":[0,3,7,9,10,15],"shell":5,"short":[0,1,2,6,10],"shorthand":0,"should":[5,14,15],"shouldn":0,"show":[2,10,12,15,16],"shut":[10,15],"shutdown":[0,4,6,9,10],"shutdwown":2,"sibl":6,"side":[7,10,11,16],"sign":[0,5,6,7,10,11,13],"signatur":[7,10,12],"signup":11,"simpl":[3,9,10,11,13,14,16],"simplecooki":0,"simpler":[7,10,11,13],"simplest":[5,7,11,13,16],"simpli":11,"simplic":[6,15],"simul":7,"sinc":[7,9],"singl":[0,2,3,6,7,10,12,15],"sit":13,"size":[0,15],"sk":11,"skip":[0,6,10],"slash":7,"sleep":[7,9],"slim":4,"slow":7,"slowlori":4,"slug":7,"small":[4,6,9,10],"smaller":4,"so":[9,10,12,13,14,15],"solv":10,"some":[4,5,10],"some_packag":13,"somemiddlewar":13,"someon":[5,7,10,16],"someth":[6,7,9,10],"sometim":[7,10],"somewher":7,"sourc":[0,5,8,10],"spawn":4,"spec":[7,10],"special":[6,10],"specif":[0,5,9,10,13,14],"specifi":3,"sphinx":8,"spin":7,"split":5,"sprawl":6,"sql":15,"sqlalchemi":[6,7,14],"sqlite":[5,15],"sse":[0,2,6],"ssl":4,"stabil":2,"stack":13,"stai":[12,15],"standard":[3,4,10,11,14],"starlett":[0,2,6,9,16],"start":[0,3,4,6,10,13,14,15],"startswith":11,"startup":[0,2,6,9,10],"state":[0,2,9,10,11],"statement":6,"static":[0,2,4,6,7,16],"static_app":0,"static_dir":[0,10],"static_rout":[0,2,10],"staticfil":0,"statu":[2,6,7,9,10,14],"status_cod":[0,6,7,9,10,11,12,13,14,15],"status_code_saf":0,"stderr":0,"stdlib":2,"still":[10,12],"stop":10,"storag":[3,6],"store":[0,5,10,11,14],"str":[0,7,9,10,11,14,15],"straightforward":10,"stranger":[0,10],"stream":[0,2,10],"stream_data":0,"stream_fil":[0,2,10],"strict":10,"string":[0,6,7,10,15,16],"strip":[2,10,11],"structur":10,"style":10,"stylesheet":10,"sub":11,"subclass":0,"subcommand":3,"subject":0,"submiss":0,"submit":0,"subrout":[6,10,12],"subsequ":[10,11],"subtl":2,"success":[0,7,14],"successfulli":14,"successor":4,"suit":9,"suppli":2,"support":[0,1,2,3,4,6,7,12,15,16],"swagger":[6,10,14],"swagger_ui":[0,10],"switch":[2,15,16],"sync":[2,6,7,12],"synchron":[0,12],"syntax":[2,6,7,12],"system":[3,7,10],"t":[0,4,5,7,9,10,11,12,13,14,15],"tabl":15,"take":[6,7],"tamper":[10,11],"target":2,"task":[0,2,6],"teach":6,"tear":10,"tell":[7,10,14,15],"templat":[0,1,2,6],"template_str":[0,7],"templates_dir":0,"termin":[3,4],"terms_of_servic":0,"test":[0,2,3,5,6,8,14],"test_500":[2,9],"test_api":9,"test_create_item":9,"test_custom_error":9,"test_echo":16,"test_head":9,"test_hello":9,"test_hook":9,"test_json":9,"test_upload":9,"test_valid":9,"test_websocket":9,"test_with_lifespan":9,"testclient":[0,2,9,16],"text":[0,2,6,7,9,10,12,16],"textcont":16,"than":[9,10,11,12,13,14],"thei":[0,5,7,10,12,13,14,16],"them":[6,7,9,10,11,12,14,16],"theme":[0,10],"themselv":10,"thi":[0,2,3,4,5,6,7,9,10,11,12,13,14,15,16],"thing":[5,6,7,9,13],"think":7,"third":6,"though":4,"thousand":4,"thread":[0,6,7,12],"threadpoolexecutor":[0,2],"three":[7,10,16],"through":[0,7,10,13,14,16],"throughput":10,"thu":0,"time":[0,2,6,7,9,10,12,13,14,15,16],"time_start":0,"timedelta":11,"timeout":4,"timingmiddlewar":13,"tip":6,"titl":[0,7,10,14,15],"tl":4,"togeth":[6,10],"token":[0,6,7,10,16],"token_hex":5,"toml":2,"too":[0,3,4,9,10],"tool":[8,13],"toolbelt":2,"toolkit":15,"top":[0,5],"total":10,"tour":[0,6,7],"trace":10,"traceback":[0,2,5,10],"track":15,"tradit":[10,11],"traffic":4,"transact":15,"transform":13,"translat":12,"transport":10,"treat":[0,10],"trick":10,"trigger":9,"true":[0,2,5,7,9,10,11,12,13,15,16],"trust":6,"trustedhostmiddlewar":[4,13],"try":[6,7,11,16],"tupl":[0,7,9],"tutori":[7,14],"two":[7,10,13],"type":[0,2,6,10,12,14,16],"typic":[7,10],"typo":[2,10],"ui":[2,6,10,14],"unauthor":[0,10,11],"under":[0,7,10,12],"understand":[6,7],"unexpectedli":16,"unhandl":13,"unicod":0,"union":2,"uniqu":10,"unit":15,"univers":5,"unknown":0,"unless":10,"unlik":10,"unmaintain":2,"unmatch":10,"unpin":2,"unrecogn":10,"until":7,"up":[0,2,7,8,10],"updat":[2,6,7,10,15,16],"update_book":[14,15],"upgrad":[2,8,10,16],"upload":[2,6,7],"url":[0,2,5,6,7,9,10,11,12,13,15],"url_for":[0,2,9],"url_prefix":12,"urllib":2,"us":[0,2,3,4,6,7,10,12,14,16],"usag":[0,2],"user":[0,5,7,10,11,12,15],"user_id":[7,10,11,12],"usernam":[10,11],"usual":9,"utcnow":11,"utf":0,"uuid":[0,2,7,10],"uuid4":0,"uv":[3,5,6,8,11,15],"uvicorn":[0,2,6,7],"uvloop":4,"v":6,"v1":[0,10],"v2":10,"valid":[0,2,6,7,10,11,13,14,15],"valu":[0,6,7,9,10,11,12,13,14,15,16],"valueerror":[0,9,10,12,14],"variabl":[0,4,6,7],"ve":[6,9,12],"venv":8,"verb":10,"veri":[0,7,15],"verifi":[9,11],"verify_token":11,"version":[0,2,9,10,14],"via":[0,2,5,10,12],"view":[0,2,6,7,9,12],"virtual":[3,4],"virtualenv":8,"visit":[10,14],"vue":10,"w":[9,10,16],"wa":[0,2,7,14],"wai":[3,4,6,7,10,11,12,13,15],"wait":12,"walk":[7,10,14],"want":[0,3,4,6,7,9,10,14,15,16],"watch":8,"we":[14,15,16],"web":[0,4,6,10,15],"websit":10,"websocket":[0,1,2,4,6],"websocket_connect":[9,16],"welcom":[6,7,11],"well":6,"went":6,"what":[0,7,9,10,16],"whatev":7,"when":[0,2,4,5,6,7,9,10,12,14,15,16],"where":[0,4,7,10,15,16],"whether":[0,7],"which":[0,2,7,9,10,12,15],"whichev":6,"while":[7,10,12,16],"whitenois":2,"who":[6,7,11],"widget":[9,10],"wildcard":10,"window":2,"wire":7,"with_app":10,"within":[2,10,15],"without":[0,2,4,7,9,10,15],"won":[7,9,10,15],"work":[3,4,5,6,7,10,13,14,15],"workdir":4,"worker":4,"world":[0,3,6,9,10,12],"worri":9,"would":9,"wrap":[0,7,9,10,13],"write":[6,7,10,12],"wrong":10,"ws_auth":16,"wsgi":[0,2,4,6,10,12],"www":5,"x":[0,4,7,9,10,11,12,13,14],"x00":16,"x01":16,"x02":16,"x89png":7,"xml":0,"xss":10,"yaml":[0,2,6,7,9,10],"year":[14,15],"yield":[0,10,15],"yml":[0,10,11],"you":[0,3,4,5,7,9,10,11,12,13,14,15,16],"your":[0,3,4,5,6,7,9,10,11,12,13],"yourself":7,"zero":4},"titles":["API Reference","Backlog","Changelog","Command Line Interface","Deployment","Configuration","Responder","Quick Start","Development Sandbox","Testing","Feature Tour","Authentication","Migrating from Flask","Writing Middleware","Building a REST API","Using SQLAlchemy","WebSocket Tutorial"],"titleterms":{"":14,"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,"In":14,"It":[5,7,14,15],"The":[0,6,12],"ad":[2,13],"after":[9,10],"all":[5,7,14],"allow":5,"api":[0,9,11,12,14],"app":[10,11],"asset":3,"auth":11,"authent":11,"background":[0,7],"backlog":1,"base":10,"bearer":11,"befor":[9,10,16],"big":12,"blueprint":12,"book":14,"build":[3,14],"built":13,"chang":2,"changelog":2,"chat":16,"class":[0,5,10],"client":16,"cloud":4,"code":0,"command":3,"configur":5,"convertor":7,"cooki":[9,10],"cor":10,"creat":[7,14],"crud":15,"custom":[3,10,11],"data":16,"databas":15,"debug":5,"defin":[14,15],"delet":14,"deploy":4,"deprec":2,"develop":8,"dict":0,"differ":12,"directli":4,"docker":4,"document":10,"echo":16,"endpoint":15,"env":5,"environ":5,"error":[9,10,11,14],"event":[9,10],"except":11,"featur":10,"file":[3,5,9,10],"filter":10,"fix":2,"fixtur":9,"flask":12,"format":16,"from":[3,12],"frontend":3,"futur":1,"get":[6,9,14],"gradual":12,"graphql":10,"group":[0,10,12],"guid":6,"handl":[9,10,14],"header":9,"hello":7,"helper":0,"hook":[9,10,13,16],"host":[5,10],"how":16,"hst":10,"html":16,"id":10,"idea":[1,6],"instal":[6,15],"instanc":3,"interfac":3,"json":[9,12],"kei":[5,11],"launch":3,"lifespan":[9,10,15],"limit":[0,10],"line":3,"list":14,"local":4,"memori":14,"messagepack":10,"method":10,"middlewar":13,"migrat":12,"mode":5,"model":[14,15],"modul":3,"mount":10,"name":3,"next":14,"openapi":10,"oper":8,"order":13,"other":10,"out":14,"paramet":[7,12],"parti":13,"pattern":5,"platform":4,"postgresql":15,"project":[6,14],"proxi":4,"public":11,"put":[5,7],"queri":0,"queue":0,"quick":[7,12],"rate":[0,10],"read":7,"refer":[0,12],"remov":2,"render":7,"request":[0,7,9,10,16],"respond":6,"respons":[0,7],"rest":14,"revers":4,"room":16,"rout":[0,7,10,11,12],"run":[4,7,14,15],"sandbox":8,"secret":5,"send":7,"sent":10,"serv":10,"server":[7,10,16],"servic":7,"session":[10,11],"setup":[8,14,15],"shutdown":15,"singl":14,"skip":11,"sqlalchemi":15,"sse":10,"starlett":13,"start":[7,9],"startup":15,"static":10,"statu":0,"storag":14,"support":10,"task":7,"templat":[7,12],"test":[9,16],"third":13,"tip":[9,15],"togeth":[5,7],"token":11,"tour":10,"trust":10,"try":14,"tutori":[6,16],"type":7,"unreleas":2,"updat":14,"upload":9,"url":3,"us":[5,9,11,13,15],"user":6,"uvicorn":4,"v":13,"v0":2,"v1":2,"v2":2,"v3":2,"valid":9,"variabl":5,"view":10,"web":[7,11],"websocket":[9,10,16],"what":[6,13,14],"when":13,"work":16,"world":7,"write":13,"you":6,"your":[14,15]}}) \ No newline at end of file diff --git a/testing.html b/testing.html index 031454d..f68e6b6 100644 --- a/testing.html +++ b/testing.html @@ -5,13 +5,13 @@ - Testing — responder 3.2.0 documentation + Testing — responder 3.4.1 documentation - + diff --git a/tour.html b/tour.html index 37d5e51..276e4a5 100644 --- a/tour.html +++ b/tour.html @@ -5,13 +5,13 @@ - Feature Tour — responder 3.2.0 documentation + Feature Tour — responder 3.4.1 documentation - + @@ -420,6 +420,21 @@ side by side, moving routes over one at a time:

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

                        +

                        You can also mount marimo notebooks as +interactive dashboards within your API:

                        +
                        import marimo
                        +
                        +server = (
                        +    marimo.create_asgi_app()
                        +    .with_app(path="", root="./notebooks/dashboard.py")
                        +    .with_app(path="/analysis", root="./notebooks/analysis.py")
                        +)
                        +
                        +api.mount("/notebooks", server.build())
                        +
                        +
                        +

                        Notebooks are served at /notebooks/ and /notebooks/analysis, +with full interactivity — reactive cells, widgets, plots, and all.

                        Cookies

                        diff --git a/tutorial-auth.html b/tutorial-auth.html new file mode 100644 index 0000000..27c305a --- /dev/null +++ b/tutorial-auth.html @@ -0,0 +1,287 @@ + + + + + + + + Authentication — responder 3.4.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
                        +
                        +
                        + + +
                        + +
                        +

                        Authentication

                        +

                        Every API that handles user data needs authentication — a way to verify +who is making a request. This guide covers the most common patterns: +API keys, JWT tokens, and how to build reusable auth guards with +Responder’s before-request hooks.

                        +
                        +

                        API Key Authentication

                        +

                        The simplest approach. The client sends a secret key in a header, and +your server checks it against a known value. This is common for +server-to-server communication and simple APIs:

                        +
                        API_KEYS = {"sk-abc123", "sk-def456"}
                        +
                        +@api.route(before_request=True)
                        +def check_api_key(req, resp):
                        +    key = req.headers.get("X-API-Key")
                        +    if key not in API_KEYS:
                        +        resp.status_code = 401
                        +        resp.media = {"error": "Invalid or missing API key"}
                        +
                        +
                        +

                        Because the before-request hook sets resp.status_code, the route +handler is skipped entirely for unauthorized requests. The client never +reaches your endpoint — the guard catches them first.

                        +

                        The client sends the key like this:

                        +
                        $ curl -H "X-API-Key: sk-abc123" http://localhost:5042/protected
                        +
                        +
                        +
                        +
                        +

                        Bearer Token Authentication

                        +

                        Bearer tokens are the standard for modern APIs. The client sends a token +in the Authorization header, and the server validates it. The most +common format is JWT (JSON Web Tokens).

                        +

                        Install PyJWT:

                        +
                        $ uv pip install pyjwt
                        +
                        +
                        +

                        Create a helper to encode and decode tokens:

                        +
                        import jwt
                        +from datetime import datetime, timedelta
                        +
                        +SECRET = "your-secret-key"
                        +
                        +def create_token(user_id: int) -> str:
                        +    payload = {
                        +        "sub": user_id,
                        +        "exp": datetime.utcnow() + timedelta(hours=24),
                        +    }
                        +    return jwt.encode(payload, SECRET, algorithm="HS256")
                        +
                        +def verify_token(token: str) -> dict | None:
                        +    try:
                        +        return jwt.decode(token, SECRET, algorithms=["HS256"])
                        +    except jwt.InvalidTokenError:
                        +        return None
                        +
                        +
                        +

                        Add a login endpoint that issues tokens, and a before-request hook that +verifies them:

                        +
                        @api.route("/login", methods=["POST"])
                        +async def login(req, resp):
                        +    data = await req.media()
                        +    # In a real app, check credentials against a database
                        +    if data.get("username") == "admin" and data.get("password") == "secret":
                        +        token = create_token(user_id=1)
                        +        resp.media = {"token": token}
                        +    else:
                        +        resp.status_code = 401
                        +        resp.media = {"error": "Invalid credentials"}
                        +
                        +@api.route(before_request=True)
                        +def auth_guard(req, resp):
                        +    # Skip auth for the login endpoint itself
                        +    if req.url.path == "/login":
                        +        return
                        +
                        +    auth = req.headers.get("Authorization", "")
                        +    if not auth.startswith("Bearer "):
                        +        resp.status_code = 401
                        +        resp.media = {"error": "Missing bearer token"}
                        +        return
                        +
                        +    token = auth[7:]  # Strip "Bearer "
                        +    payload = verify_token(token)
                        +    if payload is None:
                        +        resp.status_code = 401
                        +        resp.media = {"error": "Invalid or expired token"}
                        +        return
                        +
                        +    # Store the authenticated user on the request state
                        +    req.state.user_id = payload["sub"]
                        +
                        +
                        +

                        Now any route can access the authenticated user:

                        +
                        @api.route("/me")
                        +def get_me(req, resp):
                        +    resp.media = {"user_id": req.state.user_id}
                        +
                        +
                        +

                        The client flow:

                        +
                          +
                        1. POST /login with credentials → receive a token

                        2. +
                        3. Include Authorization: Bearer <token> on every subsequent request

                        4. +
                        5. The token expires after 24 hours — the client must log in again

                        6. +
                        +
                        +
                        +

                        Skipping Auth for Public Routes

                        +

                        The example above skips auth for /login by checking the path. For +more control, you can use a set of public paths:

                        +
                        PUBLIC_PATHS = {"/login", "/signup", "/health", "/docs", "/schema.yml"}
                        +
                        +@api.route(before_request=True)
                        +def auth_guard(req, resp):
                        +    if req.url.path in PUBLIC_PATHS:
                        +        return
                        +    # ... check token
                        +
                        +
                        +
                        +
                        +

                        Custom Exception for Auth Errors

                        +

                        For cleaner code, define a custom exception and register a handler:

                        +
                        class AuthError(Exception):
                        +    def __init__(self, message="Unauthorized", status_code=401):
                        +        self.message = message
                        +        self.status_code = status_code
                        +
                        +@api.exception_handler(AuthError)
                        +async def handle_auth_error(req, resp, exc):
                        +    resp.status_code = exc.status_code
                        +    resp.media = {"error": exc.message}
                        +
                        +
                        +

                        Now your auth guard can simply raise:

                        +
                        @api.route(before_request=True)
                        +def auth_guard(req, resp):
                        +    if req.url.path in PUBLIC_PATHS:
                        +        return
                        +    if "Authorization" not in req.headers:
                        +        raise AuthError("Missing authorization header")
                        +
                        +
                        +
                        +
                        +

                        Using Sessions for Web Apps

                        +

                        For traditional web applications (with HTML pages and forms), cookie-based +sessions are simpler than tokens. The browser handles cookies automatically +— no client-side token management needed:

                        +
                        @api.route("/login", methods=["POST"])
                        +async def login(req, resp):
                        +    data = await req.media("form")
                        +    if data["username"] == "admin" and data["password"] == "secret":
                        +        resp.session["user"] = data["username"]
                        +        api.redirect(resp, location="/dashboard")
                        +    else:
                        +        resp.status_code = 401
                        +        resp.html = "<p>Invalid credentials</p>"
                        +
                        +@api.route("/dashboard")
                        +def dashboard(req, resp):
                        +    user = req.session.get("user")
                        +    if not user:
                        +        api.redirect(resp, location="/login")
                        +        return
                        +    resp.html = f"<h1>Welcome, {user}!</h1>"
                        +
                        +@api.route("/logout")
                        +def logout(req, resp):
                        +    resp.session.clear()
                        +    api.redirect(resp, location="/login")
                        +
                        +
                        +

                        Remember to set a proper secret key:

                        +
                        api = responder.API(secret_key="your-production-secret-key")
                        +
                        +
                        +

                        The session data is signed (not encrypted) — users can read it but +can’t tamper with it. Don’t store sensitive data like passwords in +sessions.

                        +
                        +
                        + + +
                        + +
                        +
                        + +
                        +
                        + + + + + + + \ No newline at end of file diff --git a/tutorial-flask.html b/tutorial-flask.html index 1e18b68..43226d6 100644 --- a/tutorial-flask.html +++ b/tutorial-flask.html @@ -5,13 +5,13 @@ - Migrating from Flask — responder 3.2.0 documentation + Migrating from Flask — responder 3.4.1 documentation - + @@ -21,8 +21,8 @@ - - + + diff --git a/tutorial-middleware.html b/tutorial-middleware.html new file mode 100644 index 0000000..1ca3335 --- /dev/null +++ b/tutorial-middleware.html @@ -0,0 +1,222 @@ + + + + + + + + Writing Middleware — responder 3.4.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
                        +
                        +
                        + + +
                        + +
                        +

                        Writing Middleware

                        +

                        Middleware sits between the server and your route handlers, processing +every request and response that flows through your application. It’s the +right tool for cross-cutting concerns — things that apply to all +requests, not just specific routes.

                        +

                        Common middleware use cases:

                        +
                          +
                        • Request logging and timing

                        • +
                        • Authentication and authorization

                        • +
                        • Adding security headers

                        • +
                        • Request ID generation

                        • +
                        • Rate limiting

                        • +
                        • Response compression (built-in)

                        • +
                        +
                        +

                        Hooks vs. Middleware

                        +

                        Responder gives you two levels of request processing:

                        +

                        Hooks (before_request / after_request) run inside Responder’s +routing layer. They receive Responder’s req and resp objects and +are the simplest way to add behavior:

                        +
                        @api.route(before_request=True)
                        +def add_header(req, resp):
                        +    resp.headers["X-Powered-By"] = "Responder"
                        +
                        +@api.after_request()
                        +def log_request(req, resp):
                        +    print(f"{req.method} {req.url.path} -> {resp.status_code}")
                        +
                        +
                        +

                        Middleware runs at the ASGI level, wrapping the entire application. +It’s more powerful but more complex — you work with raw ASGI scopes +instead of Responder objects. Use middleware when you need to process +requests before they reach Responder’s routing, or when you need to +integrate with Starlette middleware.

                        +
                        +
                        +

                        Using Starlette Middleware

                        +

                        Responder is built on Starlette, so any Starlette middleware works +out of the box:

                        +
                        from starlette.middleware.base import BaseHTTPMiddleware
                        +
                        +class TimingMiddleware(BaseHTTPMiddleware):
                        +    async def dispatch(self, request, call_next):
                        +        import time
                        +        start = time.time()
                        +        response = await call_next(request)
                        +        duration = time.time() - start
                        +        response.headers["X-Response-Time"] = f"{duration:.3f}s"
                        +        return response
                        +
                        +api.add_middleware(TimingMiddleware)
                        +
                        +
                        +

                        The dispatch method receives a Starlette Request and a +call_next function. Call call_next(request) to pass the request +to the next middleware (or to your route handler). The return value is +a Starlette Response that you can modify before it’s sent.

                        +
                        +
                        +

                        Built-in Middleware

                        +

                        Responder configures several middleware components automatically:

                        +
                          +
                        • GZipMiddleware — compresses responses larger than 500 bytes

                        • +
                        • TrustedHostMiddleware — validates the Host header

                        • +
                        • ServerErrorMiddleware — catches unhandled exceptions

                        • +
                        • ExceptionMiddleware — routes exceptions to your handlers

                        • +
                        • SessionMiddleware — manages signed cookie sessions

                        • +
                        +

                        Optional middleware you can enable:

                        +
                          +
                        • CORSMiddlewareapi = responder.API(cors=True)

                        • +
                        • HTTPSRedirectMiddlewareapi = responder.API(enable_hsts=True)

                        • +
                        +
                        +
                        +

                        Adding Third-Party Middleware

                        +

                        Any ASGI middleware can be added with api.add_middleware():

                        +
                        from some_package import SomeMiddleware
                        +
                        +api.add_middleware(SomeMiddleware, option1="value", option2=True)
                        +
                        +
                        +

                        Keyword arguments are passed to the middleware’s constructor.

                        +
                        +
                        +

                        Middleware Order

                        +

                        Middleware wraps your application like layers of an onion. The last +middleware added is the outermost layer — it sees the request first +and the response last.

                        +

                        Responder’s built-in middleware stack (from outermost to innermost):

                        +
                          +
                        1. SessionMiddleware

                        2. +
                        3. ServerErrorMiddleware

                        4. +
                        5. CORSMiddleware (if enabled)

                        6. +
                        7. TrustedHostMiddleware

                        8. +
                        9. HTTPSRedirectMiddleware (if enabled)

                        10. +
                        11. GZipMiddleware

                        12. +
                        13. ExceptionMiddleware

                        14. +
                        15. Your routes

                        16. +
                        +

                        When you call api.add_middleware(), your middleware is added outside +the existing stack. Keep this in mind for ordering dependencies — if +middleware A depends on middleware B having run first, add B before A.

                        +
                        +
                        +

                        When to Use What

                        +
                          +
                        • Simple header additions, logging, auth checks → use hooks

                        • +
                        • Response transformation, timing, third-party integrations → use middleware

                        • +
                        • Rate limiting → use the built-in RateLimiter (it uses hooks internally)

                        • +
                        • Request ID → use api = responder.API(request_id=True)

                        • +
                        +

                        Start with hooks. They’re simpler and cover most cases. Graduate to +middleware when hooks aren’t enough.

                        +
                        +
                        + + +
                        + +
                        +
                        + +
                        +
                        + + + + + + + \ No newline at end of file diff --git a/tutorial-rest.html b/tutorial-rest.html index ebc0b75..3e69c36 100644 --- a/tutorial-rest.html +++ b/tutorial-rest.html @@ -5,13 +5,13 @@ - Building a REST API — responder 3.2.0 documentation + Building a REST API — responder 3.4.1 documentation - + diff --git a/tutorial-sqlalchemy.html b/tutorial-sqlalchemy.html index a5e2747..b4953e7 100644 --- a/tutorial-sqlalchemy.html +++ b/tutorial-sqlalchemy.html @@ -5,13 +5,13 @@ - Using SQLAlchemy — responder 3.2.0 documentation + Using SQLAlchemy — responder 3.4.1 documentation - + @@ -21,7 +21,7 @@ - + diff --git a/tutorial-websockets.html b/tutorial-websockets.html new file mode 100644 index 0000000..451b71c --- /dev/null +++ b/tutorial-websockets.html @@ -0,0 +1,261 @@ + + + + + + + + WebSocket Tutorial — responder 3.4.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
                        +
                        +
                        + + +
                        + +
                        +

                        WebSocket Tutorial

                        +

                        HTTP is request-response — the client asks, the server answers, and the +connection closes. WebSockets upgrade that into a persistent, bidirectional +channel where both sides can send messages at any time. This is what powers +chat apps, live dashboards, multiplayer games, and collaborative editors.

                        +

                        This tutorial builds a simple chat room to show how WebSockets work in +Responder.

                        +
                        +

                        How WebSockets Work

                        +
                          +
                        1. The client sends a normal HTTP request with an Upgrade: websocket +header.

                        2. +
                        3. The server accepts the upgrade and the connection switches protocols.

                        4. +
                        5. Both sides can now send messages freely — no more request/response.

                        6. +
                        7. Either side can close the connection at any time.

                        8. +
                        +

                        In Responder, WebSocket routes receive a ws object instead of +req and resp. The ws object has methods for accepting the +connection, sending and receiving data, and closing.

                        +
                        +
                        +

                        Echo Server

                        +

                        The simplest WebSocket — echoes everything back:

                        +
                        @api.route("/ws", websocket=True)
                        +async def echo(ws):
                        +    await ws.accept()
                        +    while True:
                        +        data = await ws.receive_text()
                        +        await ws.send_text(f"Echo: {data}")
                        +
                        +
                        +

                        The await ws.accept() call completes the WebSocket handshake. After +that, you’re in a loop — receive a message, send a response.

                        +

                        Test it with a WebSocket client:

                        +
                        $ pip install websocket-client
                        +$ python -c "
                        +import websocket
                        +ws = websocket.create_connection('ws://localhost:5042/ws')
                        +ws.send('hello')
                        +print(ws.recv())  # Echo: hello
                        +ws.close()
                        +"
                        +
                        +
                        +
                        +
                        +

                        Chat Room

                        +

                        A chat room needs to broadcast messages to all connected clients. We keep +a set of active connections and iterate through them when someone sends +a message:

                        +
                        connected = set()
                        +
                        +@api.route("/chat", websocket=True)
                        +async def chat(ws):
                        +    await ws.accept()
                        +    connected.add(ws)
                        +    try:
                        +        while True:
                        +            message = await ws.receive_text()
                        +            # Broadcast to all connected clients
                        +            for client in connected:
                        +                await client.send_text(message)
                        +    except Exception:
                        +        pass
                        +    finally:
                        +        connected.discard(ws)
                        +
                        +
                        +

                        The try/finally block ensures we remove disconnected clients from +the set, even if the connection drops unexpectedly.

                        +
                        +
                        +

                        Data Formats

                        +

                        WebSockets support three data formats:

                        +

                        Text — plain strings:

                        +
                        await ws.send_text("hello")
                        +message = await ws.receive_text()
                        +
                        +
                        +

                        JSON — auto-serialized Python objects:

                        +
                        await ws.send_json({"type": "update", "data": [1, 2, 3]})
                        +message = await ws.receive_json()
                        +
                        +
                        +

                        Binary — raw bytes, useful for images, audio, or custom protocols:

                        +
                        await ws.send_bytes(b"\x00\x01\x02")
                        +data = await ws.receive_bytes()
                        +
                        +
                        +
                        +
                        +

                        HTML Client

                        +

                        Here’s a minimal HTML page that connects to the chat room. The browser’s +built-in WebSocket API handles everything — no libraries needed:

                        +
                        <!DOCTYPE html>
                        +<html>
                        +<body>
                        +  <div id="messages"></div>
                        +  <input id="input" placeholder="Type a message..." />
                        +  <script>
                        +    const ws = new WebSocket("ws://localhost:5042/chat");
                        +    const messages = document.getElementById("messages");
                        +    const input = document.getElementById("input");
                        +
                        +    ws.onmessage = (event) => {
                        +      const p = document.createElement("p");
                        +      p.textContent = event.data;
                        +      messages.appendChild(p);
                        +    };
                        +
                        +    input.addEventListener("keypress", (e) => {
                        +      if (e.key === "Enter") {
                        +        ws.send(input.value);
                        +        input.value = "";
                        +      }
                        +    });
                        +  </script>
                        +</body>
                        +</html>
                        +
                        +
                        +

                        Save this as static/index.html and serve it with Responder’s +built-in static file support.

                        +
                        +
                        +

                        Before-Request Hooks for WebSockets

                        +

                        You can run code before a WebSocket connection is established, just like +HTTP before-request hooks. This is useful for authentication:

                        +
                        @api.before_request(websocket=True)
                        +async def ws_auth(ws):
                        +    # Check for a token in the query string
                        +    # (WebSocket headers are limited in browsers)
                        +    await ws.accept()
                        +
                        +
                        +

                        WebSocket before-request hooks receive the ws object and must call +await ws.accept() if they want the connection to proceed.

                        +
                        +
                        +

                        Testing WebSockets

                        +

                        Use Starlette’s TestClient for WebSocket tests:

                        +
                        from starlette.testclient import TestClient
                        +
                        +def test_echo():
                        +    client = TestClient(api)
                        +    with client.websocket_connect("/ws") as ws:
                        +        ws.send_text("hello")
                        +        assert ws.receive_text() == "Echo: hello"
                        +
                        +
                        +

                        The websocket_connect context manager handles the connection +lifecycle — it connects on enter and disconnects on exit.

                        +
                        +
                        + + +
                        + +
                        +
                        + +
                        +
                        + + + + + + + \ No newline at end of file