Compare commits

..

81 Commits

Author SHA1 Message Date
kennethreitz ce3ab46d59 Bump version to 3.5.0 and update changelog
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:27:36 -04:00
kennethreitz 6f9c87d71c Fix broad exception handling and future.result() call
- Call future.result() instead of bare property access in test (#596)
- Catch (ValueError, TypeError) instead of broad Exception in
  response model serialization (#597)
- Catch WebSocketDisconnect instead of broad Exception in
  websocket chat example (#598)

Closes #596, closes #597, closes #598

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:25:12 -04:00
kennethreitz 29d0621d98 Replace deprecated asyncio.iscoroutinefunction with inspect.iscoroutinefunction
Closes #599

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:23:24 -04:00
kennethreitz 30fa2dfda7 Add uv.lock
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:22:06 -04:00
kennethreitz 43c803a426 Restore print statements in lifespan example
These serve as illustrative markers for users reading the example.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:21:21 -04:00
Andreas Motl ff6d530338 Chore: Code formatting (#594)
## About
A few cosmetic adjustments aka. code formatting.
Also validate the outcome on CI/GHA.
Feel free to improve now or later at your disposal.

## Details
The updates are based on using the most recent versions of pyproject-fmt
and ruff.
Specifically, spots marked with `noqa` might need further love, also at
your disposal.

---------

Co-authored-by: Kenneth Reitz <me@kennethreitz.org>
2026-03-24 15:21:04 -04:00
kennethreitz a375984310 Fix WSGI mount returning 400 at mount root (#600)
## Summary
- When a WSGI app (e.g. Flask) is mounted at `/prefix` and a request
hits exactly `/prefix`, the path prefix was stripped to `""` instead of
`"/"`, causing frameworks like Flask to return 400.
- One-character fix: default the stripped path to `"/"` when empty.

## Test plan
- [x] `tests/test_responder.py::test_mount_wsgi_app` now passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:16:29 -04:00
Andreas Motl 46c6f440c5 Chore: Remove configuration for Read the Docs (#595)
https://responder.kennethreitz.org/ is here to stay.
This patch concludes the decision in GH-564.
2026-03-23 20:19:15 -04:00
Andreas Motl c87e8c876d CI: Validate on Python 3.14 vanilla, free-threaded, and PyPy (#593) 2026-03-23 18:28:27 -04:00
kennethreitz f86c7eed70 Fix RST title underline warning breaking docs CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:10:51 -04:00
kennethreitz 9d492a383c Add marimo notebook mounting docs and example
- Document mounting marimo ASGI apps in the feature tour
- Add examples/marimo_mount.py showing the integration
- Verified working: marimo.create_asgi_app() mounts cleanly via api.mount()

Fixes #580.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:09:01 -04:00
kennethreitz 77ae49aaef Improve GraphQL API interface, expand tests, drop 3.9 from CI
- Extract variables and operationName from query params and form data,
  not just JSON bodies. Fixes #571.
- Add docstrings to GraphQLView class and methods. Fixes #572.
- Add 10 new GraphQL tests: variables, operationName, mutations,
  context access, malformed queries, raw text, invalid variables
  param. Fixes #568.
- Remove Python 3.9 from CI matrix (dropped in 3.4.0).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:06:00 -04:00
kennethreitz 74c872ed57 Add type annotations to routes.py
Add comprehensive type hints to compile_path, BaseRoute, Route,
WebSocketRoute, and Router classes. Uses Starlette's Scope, Receive,
Send types and properly types the ASGI/WSGI union in Router.apps.
Fixes #566.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:47:21 -04:00
kennethreitz 724b769c9e Fix OpenAPI template packaging, add static file tests
- Include ext/openapi/docs/*.html in package_data so OpenAPI docs
  themes (swagger_ui, redoc, rapidoc, elements) ship with the wheel.
  Fixes #583.
- Add tests for static file serving and index.html fallback. Fixes #563.
- Bump version to 3.4.1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:44:02 -04:00
kennethreitz 4f02016ed6 Add comprehensive docstrings, expand API reference, upgrade to Starlette 1.0
- Add docstrings to all undocumented public methods across API, Request,
  Response, Router, Route, BackgroundQueue, and related classes
- Expand api.rst with autodoc sections for RouteGroup, BackgroundQueue,
  QueryDict, and RateLimiter
- Update starlette dependency to >=1.0
- Drop Python 3.9 support (required by Starlette 1.0), minimum is now 3.10
- Bump version to 3.4.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:37:36 -04:00
kennethreitz 3c2b1acc19 Bump version to 3.3.0 2026-03-22 13:58:18 -04:00
kennethreitz a3b49ab9fd Add auth, WebSocket, middleware, config guides. Examples and changelog.
New documentation pages:
- Authentication: API keys, JWT tokens, session auth, custom exceptions
- WebSocket Tutorial: echo server, chat room, HTML client, data formats
- Writing Middleware: hooks vs middleware, Starlette integration, ordering
- Configuration: env vars, .env files, secret keys, debug mode, production setup

Also:
- Complete working example at end of quickstart with cross-references
- Three new example files: rest_api.py, websocket_chat.py, sse_stream.py
- Changelog updated for v3.2.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:56:35 -04:00
kennethreitz bf17b02653 Add tutorials: REST API, SQLAlchemy, Flask migration. Rewrite CLI and API ref.
Three new tutorial pages:
- Building a REST API: full CRUD with Pydantic validation, from scratch
- Using SQLAlchemy: async engine, lifespan setup, CRUD with ORM
- Migrating from Flask: concept mapping, quick reference table,
  gradual migration via app mounting

Also rewritten:
- CLI docs: cleaner, more concise
- API reference: added prose descriptions for each section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:34:01 -04:00
kennethreitz 9383cd0f16 Rework homepage prose for better flow
Lead with recognition, earn each paragraph, land on the welcome.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:09:46 -04:00
kennethreitz 226bd63ed3 Full docs pass: educational prose throughout all pages
Every section now teaches web development concepts alongside the code:
- HTTP methods, status codes, content negotiation explained
- What ASGI is and why it matters
- How cookies, sessions, CORS, and HSTS work
- When to use WebSockets vs SSE
- Why request validation matters
- How background tasks differ from task queues
- What rate limiting protects against
- What Host header injection is
- Separation of concerns in templating

People should learn about web development while reading these docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:07:56 -04:00
kennethreitz b3c55f68d9 Expand testing docs with prose, examples, and tips
New sections: request validation, headers/cookies, custom error
handlers, before/after hooks, and practical tips. Every section
now explains the why, not just the how.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:00:29 -04:00
kennethreitz a2a2ae21ff Good intentions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:58:05 -04:00
kennethreitz 84074860aa Add note about Responder's spirit as a passion project
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:55:36 -04:00
kennethreitz 21baa03640 CI: Add CNAME for custom domain in GitHub Pages deploy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:51:14 -04:00
kennethreitz 0c552e25cb CI: Deploy docs to GitHub Pages on push to main
Uses peaceiris/actions-gh-pages to push built docs to gh-pages branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:48:05 -04:00
kennethreitz 24958bff51 v3.2.0: Request ID, rate limiting, MessagePack, docs update
New features:
- Request ID: api = responder.API(request_id=True)
- Rate limiting: RateLimiter(requests=100, period=60).install(api)
- MessagePack format: await req.media("msgpack")
- All new features documented in tour

176 tests, 95% coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:45:50 -04:00
kennethreitz 364f6b67f7 Add auto-validation, SSE, stream_file, after_request, route groups
Five new features:

1. **Pydantic auto-validation** — if request_model is set, request
   bodies are validated automatically and 422 returned on failure.
   If response_model is set, resp.media is serialized through the
   model (extra fields stripped, types enforced).

2. **Server-Sent Events** — resp.sse for real-time streaming:

       @resp.sse
       async def stream():
           yield {"event": "update", "data": "hello"}

3. **resp.stream_file()** — stream large files without loading
   into memory, with automatic content-type detection.

4. **after_request hooks** — run code after every request:

       @api.after_request()
       def add_request_id(req, resp):
           resp.headers["X-Request-ID"] = str(uuid.uuid4())

5. **Route groups** — organize routes with shared prefixes:

       v1 = api.group("/v1")

       @v1.route("/users")
       def list_users(req, resp): ...

Also fix streaming responses not sending Content-Type headers.

172 tests, 95% coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:42:48 -04:00
kennethreitz 2cab7b5af7 Document Pydantic OpenAPI support in feature tour
Three approaches: Pydantic models (recommended), YAML docstrings,
and marshmallow schemas — all work together.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:36:02 -04:00
kennethreitz 1bfd85b003 Add Pydantic support for OpenAPI schema generation
Define your API schemas with Pydantic models instead of (or alongside)
YAML docstrings and marshmallow:

    from pydantic import BaseModel

    class PetIn(BaseModel):
        name: str
        age: int = 0

    class PetOut(BaseModel):
        id: int
        name: str
        age: int

    @api.route("/pets", methods=["POST"],
               request_model=PetIn, response_model=PetOut)
    async def create_pet(req, resp):
        data = await req.media()
        resp.media = {"id": 1, **data}

Also works with @api.schema("Name") decorator for registering
standalone schema components.

Pydantic models, marshmallow schemas, and YAML docstrings can all
be used together in the same API.

Also: rewrite docs with more prose, restore sidebar logo and links,
add FastAPI acknowledgment, update homepage copy.

161 tests, 95% coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:35:07 -04:00
kennethreitz 33ebc77f10 Rewrite docs from scratch, remove 14MB of bundled fonts
Complete documentation rewrite:
- Clean, code-first index page (no badges, no testimonials)
- Rewritten quickstart: request/response reference, templates, background tasks
- Rewritten feature tour: method filtering, lifespan, file serving,
  error handling, hooks, WebSockets, GraphQL, OpenAPI, CORS, sessions
- Simplified testing and deployment guides
- Stripped conf.py to essential extensions only

Removed cruft:
- 14MB of paid font files (Mercury, Operator Mono)
- Google Analytics (deprecated Universal Analytics)
- UserVoice widget
- Konami code easter egg
- Algolia DocSearch (not configured)
- Twitter widgets
- Unused Sphinx extensions (mathjax, ifconfig, coverage, doctest, opengraph)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:17:22 -04:00
kennethreitz 30801557a3 Bump version to 3.1.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 07:57:32 -04:00
kennethreitz 73d46e9b03 CI: Remove linkcheck from docs workflow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 07:54:59 -04:00
kennethreitz 3d65d88ea9 Rewrite README from scratch
Clean, code-first README. No badges, no testimonials — just
show what the framework does with real examples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 07:53:25 -04:00
kennethreitz 8f979719a0 Remove poethepoet, use direct commands in CI
Replace all poe task runner usage with direct pytest/sphinx/ruff
commands. Remove poethepoet dependency and [tool.poe.tasks] config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 07:50:32 -04:00
kennethreitz 0cbcaf9c4f Code quality improvements and test fixes (#592)
## Summary
Comprehensive post-v3.0.0 modernization: new features, bug fixes,
dependency cleanup, docs, and test coverage.

**New features:**
- **HTTP method filtering** — `@api.route("/data", methods=["GET"])`
- **Lifespan context manager** — modern async startup/shutdown
- **`api.exception_handler()`** — custom error handling per exception
type
- **`api.graphql()`** — one-liner GraphQL setup
- **`resp.file()`** — serve files from disk with auto content-type
- **before_request short-circuit** — set status code to skip route
handler
- **`req.path_params`** / **`req.client`** / **`req.is_json`** — new
request properties
- **`uuid`** and **`path`** route convertors
- **PEP 561 `py.typed`** marker

**Bug fixes:**
- Fix multipart parser losing headers
- Fix `url_for()` with typed params (`{id:int}`)
- Fix `resp.body` encoding crash on bytes content
- Fix Python 3.9 type syntax (`from __future__ import annotations`)
- Fix broken session test and no-op file upload test
- Fix helloworld example 404 on root path

**Dependencies:**
- Flattened — `pip install responder` gets everything
- Core: just starlette + uvicorn (down from 10 deps)

**Docs & README:**
- All new features documented in tour
- Modernized README features list
- Deployment guide: Docker, cloud, uvicorn
- Removed Pipenv, extras, stale references throughout

**Tests & quality:**
- 117 tests (up from 92), 91% coverage, 0 warnings
- CaseInsensitiveDict, GraphQL edge cases, staticfiles tests
- Ruff clean, all `tmpdir` → `tmp_path`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 07:44:11 -04:00
kennethreitz 3fa6f11ffa Clean up stale comments, dead test code, and flaky npm assertions
- Remove commented-out tests (route overlap, form data, file uploads)
- Remove stale TODO/FIXME comments from routes.py and api.py
- Make CLI npm error assertions case-insensitive and more flexible
  to handle different npm versions across CI environments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 8b88b148bf Remove requests/requests-toolbelt from test code
Replace with stdlib urllib.request. No third-party HTTP client
needed for test probing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 1aecafa82a Update CHANGELOG for v3.0.0 release
Reflect all changes made in this branch: dependency reduction,
GraphQL modernization, pyproject.toml migration, etc. Also fix
compare links to point to kennethreitz/responder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 8c763aa97e Migrate from setup.py to declarative pyproject.toml
All package metadata now lives in pyproject.toml. Removes setup.py
and MANIFEST.in. Also exports __version__ from the package root.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 91aa242a5a Fix python-multipart import deprecation warning
Use `python_multipart` import name instead of deprecated `multipart`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 084d057a99 Move apispec and marshmallow to openapi extra
These are only used by the OpenAPI extension, not core responder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz d3acf2c1c1 Drop rfc3986 dep, clean up internals
- Replace rfc3986 with stdlib urllib.parse
- Remove deprecated status code aliases (resume_incomplete/resume)
  that were marked for removal in 3.0
- Remove private ThreadPoolExecutor API usage in BackgroundQueue
- Clean up stale comments (old Starlette PR refs, requests attribution)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 80715a12ac Drop requests dependency, modernize GraphQL, clean up setup.py
- Remove `requests` as a core dependency — replaced CaseInsensitiveDict
  and RequestsCookieJar with lightweight stdlib implementations
- Remove `requests-toolbelt` — replaced multipart decoder with
  python-multipart (already a starlette transitive dep)
- Upgrade GraphQL to graphene 3 + graphql-core 3, drop graphql-server-core
- Update GraphiQL template from 0.12.0 (2018) to 3.0.6 with React 18
- Clean up setup.py: remove dead DebCommand, UploadCommand, publish hack
- Remove linting from `poe check` (tests only)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 66fc7afbe4 CI: Simplify test matrix to Ubuntu-only
No need to test on all three OSes — this is a pure Python web
framework with no platform-specific code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz e7776eb9e8 v3.0.0: Modernize for latest Starlette, drop EOL Pythons
- Bump version to 3.0.0
- Replace deprecated starlette.middleware.wsgi with a2wsgi
- Pin starlette[full]>=0.40, add a2wsgi dependency
- Bump minimum Python to 3.9, drop 3.7/3.8 support
- Remove whitenoise conditional (Python <3.8 no longer supported)
- Clean up CI matrix: drop old Python versions and OS exclusions
- Fix deprecated httpx TestClient usage in tests (data= -> content=, per-request cookies)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
Andreas Motl 944d47da45 CI: Remove pip caching. Has woes. 2025-02-03 00:43:10 +01:00
Andreas Motl a3a12cff77 Chore: Add another basic example 2025-02-03 00:43:10 +01:00
Andreas Motl 7b2839086d Thanks, JetBrains. 2025-01-21 23:19:09 +01:00
Andreas Motl 351ff8d95e Documentation: Copy editing, this and that 2025-01-19 05:21:46 +01:00
Andreas Motl 2278beba18 SFA: Update to pueblo[sfa] 0.0.11 2025-01-19 05:02:47 +01:00
Andreas Motl 3cfc7ec2b6 Chore: Remove support for EOL Python 3.6 2025-01-19 05:02:47 +01:00
Andreas Motl 0de22eeed2 SFA: Use application loader from pueblo.sfa 2025-01-19 05:02:47 +01:00
Andreas Motl b0cc37861b SFA: Unlock loading application from remote location, using fsspec 2025-01-19 05:02:47 +01:00
Andreas Motl 7d4532acc9 CI: Use GHA recipe astral-sh/setup-uv, and more 2025-01-18 22:56:50 +01:00
Andreas Motl 1b63d2943a Chore: A few updates from code review etc. 2025-01-18 22:22:36 +01:00
Andreas Motl b5723303c8 CI: The macOS-12 environment is deprecated 2025-01-18 22:22:36 +01:00
Andreas Motl 5730be4b31 Chore: Format code using most recent ruff and pyproject-fmt 2025-01-18 22:22:36 +01:00
Andreas Motl 6f9c11645a CLI: Load from file or module. Add software tests and documentation.
Also, refactor to `responder.ext.cli`.
2025-01-18 22:22:36 +01:00
kennethreitz 827cc64988 CLI: Re-add command line interface (2024)
Install: pip install 'responder[cli]'

The CLI is an optional subsystem from now on.
2025-01-18 22:22:36 +01:00
Andreas Motl 7b5db5bc33 uvicorn: Fix uvicorn.run invocation re. debug argument
The `debug` argument no longer exists. Let's adjust the `log_level` to
`debug` instead.
2025-01-18 22:22:36 +01:00
kennethreitz b9a03c7088 Create FUNDING.yml
Signed-off-by: Kenneth Reitz <me@kennethreitz.org>
2024-11-17 06:28:31 -05:00
Andreas Motl 4cbf55508e Documentation: Update change log for upcoming version 3.0.0 2024-10-31 09:51:29 +01:00
Andreas Motl 83d0fcf1ae Documentation: Update developer sandbox documentation 2024-10-31 09:51:29 +01:00
Taoufik a698eaaab3 GraphQL: Re-add extension and dependencies (2024) 2024-10-31 06:36:13 +01:00
Andreas Motl 3aa21eed08 OpenAPI: Refactor module to responder.ext.openapi
It has been `responder.ext.schema` before.
2024-10-30 23:45:55 +01:00
Andreas Motl 2741c74b90 OpenAPI: Make extension optional
Install with: pip install 'responder[openapi]'
2024-10-30 23:45:55 +01:00
Andreas Motl aba96525ad Dependencies: Migrate from WhiteNoise to ServeStatic 2024-10-30 23:21:23 +01:00
Andreas Motl a5b6d36991 Sandbox: Enable mypy type checker 2024-10-30 23:12:11 +01:00
Andreas Motl e4cff76fa6 Documentation: Unlock writing in Markdown, using Sphinx/MyST 2024-10-30 21:23:12 +01:00
Andreas Motl f11ad7136d Documentation: Add Sphinx extensions "copybutton" and "opengraph" 2024-10-30 21:23:12 +01:00
Andreas Motl c32e8c7468 Documentation: Refactor Sphinx dependencies into setup.py 2024-10-30 20:42:10 +01:00
Andreas Motl d93e3cd12c Documentation: Update Read the Docs (RTD) configuration 2024-10-30 20:42:10 +01:00
Andreas Motl 040f1a57e4 Dependencies: Remove aiofiles
Apparently, it is not used.
2024-10-28 16:36:46 +01:00
dependabot[bot] 307313744f Update alabaster requirement from <0.8 to <1.1
Updates the requirements on [alabaster](https://github.com/sphinx-doc/alabaster) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/alabaster/releases)
- [Changelog](https://github.com/sphinx-doc/alabaster/blob/master/docs/changelog.rst)
- [Commits](https://github.com/sphinx-doc/alabaster/compare/0.1.0...1.0.0)

---
updated-dependencies:
- dependency-name: alabaster
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 10:47:51 +01:00
Andreas Motl 98ca45003b Documentation: Badges, linking, wording, inline comments. This and that.
A few of the adjustments here have been required to mitigate Sphinx
warnings, which would converge to errors on CI, thus failing the build.

A few other changes, both wording and syntax/formatting fixes, are
coming from regular copyediting and documentation maintenance.
2024-10-27 18:13:13 +01:00
Andreas Motl ab76594297 CI: Run link checker and build documentation as GHA workflow 2024-10-27 18:13:13 +01:00
Andreas Motl 7fba0f6362 Fix dispatching static_route=None on Windows 2024-10-26 05:20:57 -04:00
Andreas Motl 4ff73e9d0c Sandbox: Bring back python setup.py publish subcommand
It has been removed too early.
2024-10-26 05:16:12 -04:00
Andreas Motl 68bbea0a55 CI: Validate on Python 3.6+
You never know how you possibly save someone's life with that.
2024-10-26 00:42:07 +02:00
Andreas Motl 106e5e9073 CI: Validate on Windows operating system 2024-10-26 00:27:44 +02:00
Andreas Motl 3426aa71da Documentation: Fix broken links in README 2024-10-25 12:13:08 -04:00
136 changed files with 10518 additions and 6510 deletions
+3
View File
@@ -0,0 +1,3 @@
github: kennethreitz
thanks_dev: kennethreitz
custom: https://cash.app/$KennethReitz
+53
View File
@@ -0,0 +1,53 @@
name: "Documentation"
on:
push:
branches: [ main ]
pull_request: ~
workflow_dispatch:
# Cancel redundant in-progress jobs.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
documentation:
name: "Documentation"
runs-on: ubuntu-latest
env:
UV_SYSTEM_PYTHON: true
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Set up uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
enable-cache: true
cache-dependency-glob: |
pyproject.toml
- name: Install package and documentation dependencies
run: uv pip install '.[docs]'
- name: Build static HTML documentation
run: sphinx-build -W --keep-going docs/source docs/build
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/build
cname: responder.kennethreitz.org
+30 -13
View File
@@ -12,30 +12,47 @@ concurrency:
cancel-in-progress: true
jobs:
test:
name: "Python ${{ matrix.python-version }} on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
name: "Python ${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [
"ubuntu-latest",
"macos-12",
"macos-latest",
]
python-version: [
"3.10",
"3.11",
"3.12",
"3.13",
"pypy3.10",
"3.14",
"3.14t",
"pypy3.11",
]
env:
UV_SYSTEM_PYTHON: true
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: yezz123/setup-uv@v4
- run: uv pip install --editable '.[graphql,develop,test]' --system
- run: poe check
- name: Set up uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
enable-cache: true
cache-suffix: ${{ matrix.python-version }}
cache-dependency-glob: |
pyproject.toml
- name: Install package
run: uv pip install '.[develop,test]'
- name: Run tests
run: pytest
+1
View File
@@ -7,6 +7,7 @@
.pytest_cache
.DS_Store
coverage.xml
.coverage*
__pycache__
tests/__pycache__
+180 -46
View File
@@ -7,6 +7,135 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased]
## [v3.5.0] - 2026-03-24
### Added
- CI validation for Python 3.14, 3.14 free-threaded, and PyPy 3.11
- Marimo notebook mounting docs and example
- Type annotations for `routes.py`
- `uv.lock` for reproducible installs
### Changed
- Replaced deprecated `asyncio.iscoroutinefunction` with `inspect.iscoroutinefunction` ahead of Python 3.16 removal
- Narrowed broad `except Exception` to specific exceptions in response model serialization and websocket chat example
- Improved GraphQL API interface with expanded test coverage
- Code formatting cleanup via pyproject-fmt and ruff
- Dropped Python 3.9 from CI
### Fixed
- WSGI mount returning 400 when requesting the exact mount root path
- Werkzeug 3.1.7 compatibility for trusted host validation in tests
- `future.result` bare property access in background task test (now properly calls `future.result()`)
- OpenAPI template packaging and static file serving
- RST title underline warning breaking docs CI
### Removed
- Read the Docs configuration (docs hosted on GitHub Pages)
## [v3.4.0] - 2026-03-22
### Changed
- Upgraded to Starlette 1.0
- Added comprehensive docstrings across the codebase
- Expanded API reference documentation
## [v3.3.0] - 2026-03-22
### Added
- Full documentation rewrite: tutorials for REST APIs, SQLAlchemy, Flask migration
- Auth, WebSocket, middleware, and configuration guides
- Testing docs with prose, examples, and tips
- GitHub Pages deployment for docs
### Changed
- Reworked homepage prose
- Rewrote CLI and API reference docs
## [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
- Platform: Minimum Python version is now 3.9 (dropped 3.6, 3.7, 3.8)
- Dependencies: Dramatically reduced core dependency count (10 → 5)
- Removed `requests`, `requests-toolbelt`, `rfc3986`, `whitenoise`
- Moved `apispec` and `marshmallow` to `openapi` optional extra
- Replaced `rfc3986` with stdlib `urllib.parse`
- Replaced `requests-toolbelt` multipart decoder with `python-multipart`
- Replaced deprecated `starlette.middleware.wsgi` with `a2wsgi`
- Switched from WhiteNoise to ServeStatic
- Dependencies: Pinned `starlette[full]>=0.40` (was unpinned)
- GraphQL: Upgraded to `graphene>=3` and `graphql-core>=3.1`
(from `graphene<3` and `graphql-server-core`, which is unmaintained)
- GraphQL: Updated GraphiQL UI from 0.12.0 (2018) to 3.0.6 with React 18
- Extensions: All of CLI-, GraphQL-, and OpenAPI-Support modules are
extensions now, found within the `responder.ext` module namespace.
- Packaging: Migrated from `setup.py` to declarative `pyproject.toml`
### Removed
- Platform: Removed support for EOL Python 3.6, 3.7, 3.8
- Status codes: Removed deprecated `resume_incomplete` and `resume`
aliases for HTTP 308 (marked for removal in 3.0)
- CLI: `responder run --build` ceased to exist
### Fixed
- Routing: Fixed dispatching `static_route=None` on Windows
- uvicorn: `--debug` now maps to uvicorn's `log_level = "debug"`
- Tests: Fixed deprecated httpx TestClient usage
## [v2.0.5] - 2019-12-15
### Added
@@ -333,49 +462,54 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Conception!
[unreleased]: https://github.com/taoufik07/responder/compare/v2.0.5..HEAD
[v2.0.5]: https://github.com/taoufik07/responder/compare/v2.0.4..v2.0.5
[v2.0.4]: https://github.com/taoufik07/responder/compare/v2.0.3..v2.0.4
[v2.0.3]: https://github.com/taoufik07/responder/compare/v2.0.2..v2.0.3
[v2.0.2]: https://github.com/taoufik07/responder/compare/v2.0.1..v2.0.2
[v2.0.1]: https://github.com/taoufik07/responder/compare/v2.0.0..v2.0.1
[v2.0.0]: https://github.com/taoufik07/responder/compare/v1.3.2..v2.0.0
[v1.3.2]: https://github.com/taoufik07/responder/compare/v1.3.1..v1.3.2
[v1.3.1]: https://github.com/taoufik07/responder/compare/v1.3.0..v1.3.1
[v1.3.0]: https://github.com/taoufik07/responder/compare/v1.2.0..v1.3.0
[v1.2.0]: https://github.com/taoufik07/responder/compare/v1.1.3..v1.2.0
[v1.1.3]: https://github.com/taoufik07/responder/compare/v1.1.2..v1.1.3
[v1.1.2]: https://github.com/taoufik07/responder/compare/v1.1.1..v1.1.2
[v1.1.1]: https://github.com/taoufik07/responder/compare/v1.1.0..v1.1.1
[v1.1.0]: https://github.com/taoufik07/responder/compare/v1.0.5..v1.1.0
[v1.0.5]: https://github.com/taoufik07/responder/compare/v1.0.4..v1.0.5
[v1.0.4]: https://github.com/taoufik07/responder/compare/v1.0.3..v1.0.4
[v1.0.3]: https://github.com/taoufik07/responder/compare/v1.0.2..v1.0.3
[v1.0.2]: https://github.com/taoufik07/responder/compare/v1.0.1..v1.0.2
[v1.0.1]: https://github.com/taoufik07/responder/compare/v1.0.0..v1.0.1
[v1.0.0]: https://github.com/taoufik07/responder/compare/v0.3.3..v1.0.0
[v0.3.3]: https://github.com/taoufik07/responder/compare/v0.3.2..v0.3.3
[v0.3.2]: https://github.com/taoufik07/responder/compare/v0.3.1..v0.3.2
[v0.3.1]: https://github.com/taoufik07/responder/compare/v0.3.0..v0.3.1
[v0.3.0]: https://github.com/taoufik07/responder/compare/v0.2.3..v0.3.0
[v0.2.3]: https://github.com/taoufik07/responder/compare/v0.2.2..v0.2.3
[v0.2.2]: https://github.com/taoufik07/responder/compare/v0.2.1..v0.2.2
[v0.2.1]: https://github.com/taoufik07/responder/compare/v0.2.0..v0.2.1
[v0.2.0]: https://github.com/taoufik07/responder/compare/v0.1.6..v0.2.0
[v0.1.6]: https://github.com/taoufik07/responder/compare/v0.1.5..v0.1.6
[v0.1.5]: https://github.com/taoufik07/responder/compare/v0.1.4..v0.1.5
[v0.1.4]: https://github.com/taoufik07/responder/compare/v0.1.3..v0.1.4
[v0.1.3]: https://github.com/taoufik07/responder/compare/v0.1.2..v0.1.3
[v0.1.2]: https://github.com/taoufik07/responder/compare/v0.1.1..v0.1.2
[v0.1.1]: https://github.com/taoufik07/responder/compare/v0.1.0..v0.1.1
[v0.1.0]: https://github.com/taoufik07/responder/compare/v0.0.10..v0.1.0
[v0.0.10]: https://github.com/taoufik07/responder/compare/v0.0.9..v0.0.10
[v0.0.9]: https://github.com/taoufik07/responder/compare/v0.0.8..v0.0.9
[v0.0.8]: https://github.com/taoufik07/responder/compare/v0.0.7..v0.0.8
[v0.0.7]: https://github.com/taoufik07/responder/compare/v0.0.6..v0.0.7
[v0.0.6]: https://github.com/taoufik07/responder/compare/v0.0.5..v0.0.6
[v0.0.5]: https://github.com/taoufik07/responder/compare/v0.0.4..v0.0.5
[v0.0.4]: https://github.com/taoufik07/responder/compare/v0.0.3..v0.0.4
[v0.0.3]: https://github.com/taoufik07/responder/compare/v0.0.2..v0.0.3
[v0.0.2]: https://github.com/taoufik07/responder/compare/v0.0.1..v0.0.2
[v0.0.1]: https://github.com/taoufik07/responder/compare/v0.0.0..v0.0.1
[unreleased]: https://github.com/kennethreitz/responder/compare/v3.5.0..HEAD
[v3.5.0]: https://github.com/kennethreitz/responder/compare/v3.4.0..v3.5.0
[v3.4.0]: https://github.com/kennethreitz/responder/compare/v3.3.0..v3.4.0
[v3.3.0]: https://github.com/kennethreitz/responder/compare/v3.2.0..v3.3.0
[v3.2.0]: https://github.com/kennethreitz/responder/compare/v3.0.0..v3.2.0
[v3.0.0]: https://github.com/kennethreitz/responder/compare/v2.0.5..v3.0.0
[v2.0.5]: https://github.com/kennethreitz/responder/compare/v2.0.4..v2.0.5
[v2.0.4]: https://github.com/kennethreitz/responder/compare/v2.0.3..v2.0.4
[v2.0.3]: https://github.com/kennethreitz/responder/compare/v2.0.2..v2.0.3
[v2.0.2]: https://github.com/kennethreitz/responder/compare/v2.0.1..v2.0.2
[v2.0.1]: https://github.com/kennethreitz/responder/compare/v2.0.0..v2.0.1
[v2.0.0]: https://github.com/kennethreitz/responder/compare/v1.3.2..v2.0.0
[v1.3.2]: https://github.com/kennethreitz/responder/compare/v1.3.1..v1.3.2
[v1.3.1]: https://github.com/kennethreitz/responder/compare/v1.3.0..v1.3.1
[v1.3.0]: https://github.com/kennethreitz/responder/compare/v1.2.0..v1.3.0
[v1.2.0]: https://github.com/kennethreitz/responder/compare/v1.1.3..v1.2.0
[v1.1.3]: https://github.com/kennethreitz/responder/compare/v1.1.2..v1.1.3
[v1.1.2]: https://github.com/kennethreitz/responder/compare/v1.1.1..v1.1.2
[v1.1.1]: https://github.com/kennethreitz/responder/compare/v1.1.0..v1.1.1
[v1.1.0]: https://github.com/kennethreitz/responder/compare/v1.0.5..v1.1.0
[v1.0.5]: https://github.com/kennethreitz/responder/compare/v1.0.4..v1.0.5
[v1.0.4]: https://github.com/kennethreitz/responder/compare/v1.0.3..v1.0.4
[v1.0.3]: https://github.com/kennethreitz/responder/compare/v1.0.2..v1.0.3
[v1.0.2]: https://github.com/kennethreitz/responder/compare/v1.0.1..v1.0.2
[v1.0.1]: https://github.com/kennethreitz/responder/compare/v1.0.0..v1.0.1
[v1.0.0]: https://github.com/kennethreitz/responder/compare/v0.3.3..v1.0.0
[v0.3.3]: https://github.com/kennethreitz/responder/compare/v0.3.2..v0.3.3
[v0.3.2]: https://github.com/kennethreitz/responder/compare/v0.3.1..v0.3.2
[v0.3.1]: https://github.com/kennethreitz/responder/compare/v0.3.0..v0.3.1
[v0.3.0]: https://github.com/kennethreitz/responder/compare/v0.2.3..v0.3.0
[v0.2.3]: https://github.com/kennethreitz/responder/compare/v0.2.2..v0.2.3
[v0.2.2]: https://github.com/kennethreitz/responder/compare/v0.2.1..v0.2.2
[v0.2.1]: https://github.com/kennethreitz/responder/compare/v0.2.0..v0.2.1
[v0.2.0]: https://github.com/kennethreitz/responder/compare/v0.1.6..v0.2.0
[v0.1.6]: https://github.com/kennethreitz/responder/compare/v0.1.5..v0.1.6
[v0.1.5]: https://github.com/kennethreitz/responder/compare/v0.1.4..v0.1.5
[v0.1.4]: https://github.com/kennethreitz/responder/compare/v0.1.3..v0.1.4
[v0.1.3]: https://github.com/kennethreitz/responder/compare/v0.1.2..v0.1.3
[v0.1.2]: https://github.com/kennethreitz/responder/compare/v0.1.1..v0.1.2
[v0.1.1]: https://github.com/kennethreitz/responder/compare/v0.1.0..v0.1.1
[v0.1.0]: https://github.com/kennethreitz/responder/compare/v0.0.10..v0.1.0
[v0.0.10]: https://github.com/kennethreitz/responder/compare/v0.0.9..v0.0.10
[v0.0.9]: https://github.com/kennethreitz/responder/compare/v0.0.8..v0.0.9
[v0.0.8]: https://github.com/kennethreitz/responder/compare/v0.0.7..v0.0.8
[v0.0.7]: https://github.com/kennethreitz/responder/compare/v0.0.6..v0.0.7
[v0.0.6]: https://github.com/kennethreitz/responder/compare/v0.0.5..v0.0.6
[v0.0.5]: https://github.com/kennethreitz/responder/compare/v0.0.4..v0.0.5
[v0.0.4]: https://github.com/kennethreitz/responder/compare/v0.0.3..v0.0.4
[v0.0.3]: https://github.com/kennethreitz/responder/compare/v0.0.2..v0.0.3
[v0.0.2]: https://github.com/kennethreitz/responder/compare/v0.0.1..v0.0.2
[v0.0.1]: https://github.com/kennethreitz/responder/compare/v0.0.0..v0.0.1
-33
View File
@@ -1,33 +0,0 @@
# Development Sandbox
## Setup
Acquire sources and install project in editable mode.
```shell
git clone https://github.com/kennethreitz/responder
cd responder
python3 -m venv .venv
source .venv/bin/activate
pip install --editable '.[graphql,develop,release,test]'
```
## Operations
Invoke linter and software tests.
```shell
poe check
```
Format code.
```shell
poe format
```
## Release
```shell
git tag v2.1.0
git push --tags
poe release
```
-1
View File
@@ -1 +0,0 @@
include LICENSE
+86 -68
View File
@@ -1,91 +1,109 @@
# Responder: a familiar HTTP Service Framework for Python
# Responder
[![Build Status](https://github.com/kennethreitz/responder/actions/workflows/test.yaml/badge.svg)](https://github.com/kennethreitz/responder/actions/workflows/test.yaml)
[![Documentation Status](https://github.com/kennethreitz/responder/actions/workflows/pages/pages-build-deployment/badge.svg)](https://responder.kennethreitz.org/)
[![image](https://img.shields.io/pypi/v/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/pypi/l/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/pypi/pyversions/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/github/contributors/kennethreitz/responder.svg)](https://github.com/kennethreitz/responder/graphs/contributors)
[![PyPI Downloads](https://pepy.tech/badge/responder/month)](https://pepy.tech/project/responder/)
[![Status](https://img.shields.io/pypi/status/responder.svg)](https://pypi.org/project/responder/)
[![License](https://img.shields.io/pypi/l/responder.svg)](https://pypi.org/project/responder/)
A familiar HTTP Service Framework for Python, powered by [Starlette](https://www.starlette.io/).
[![](https://farm2.staticflickr.com/1959/43750081370_a4e20752de_o_d.png)](https://responder.readthedocs.io)
```python
import responder
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional.
[View documentation](https://responder.readthedocs.io).
api = responder.API()
This gets you a ASGI app, with a production static files server pre-installed, jinja2
templating (without additional imports), and a production webserver based on uvloop,
serving up requests with gzip compression automatically.
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
## Testimonials
if __name__ == "__main__":
api.run()
```
> "Pleasantly very taken with python-responder.
> [@kennethreitz](https://twitter.com/kennethreitz) at his absolute best." —Rudraksh
> M.K.
$ pip install responder
> "ASGI is going to enable all sorts of new high-performance web services. It's awesome
> to see Responder starting to take advantage of that." — Tom Christie author of
> [Django REST Framework](https://www.django-rest-framework.org/)
That's it. Supports Python 3.10+.
> "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of
> [Two Scoops of Django]()
## The Basics
## More Examples
- `resp.text` sends back text. `resp.html` sends back HTML. `resp.content` sends back bytes.
- `resp.media` sends back JSON (or YAML, with content negotiation).
- `resp.file("path.pdf")` serves a file with automatic content-type detection.
- `req.headers` is case-insensitive. `req.params` gives you query parameters.
- Both sync and async views work — the `async` is optional.
See
[the documentation's feature tour](https://responder.readthedocs.io/en/latest/tour.html)
for more details on features available in Responder.
## Highlights
# Installing Responder
```python
# Type-safe route parameters
@api.route("/users/{user_id:int}")
async def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
Install the most recent stable release:
# HTTP method filtering
@api.route("/items", methods=["POST"])
async def create_item(req, resp):
data = await req.media()
resp.media = {"created": data}
pip install --upgrade responder
# Class-based views
@api.route("/things/{id}")
class ThingResource:
def on_get(self, req, resp, *, id):
resp.media = {"id": id}
def on_post(self, req, resp, *, id):
resp.text = "created"
Or, install directly from the repository:
# Before-request hooks (auth, rate limiting, etc.)
@api.route(before_request=True)
def check_auth(req, resp):
if not req.headers.get("Authorization"):
resp.status_code = 401
resp.media = {"error": "unauthorized"}
pip install 'responder @ git+https://github.com/kennethreitz/responder.git'
# Custom error handling
@api.exception_handler(ValueError)
async def handle_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
Only **Python 3.6+** is supported.
# Lifespan events
from contextlib import asynccontextmanager
# The Basic Idea
@asynccontextmanager
async def lifespan(app):
print("starting up")
yield
print("shutting down")
The primary concept here is to bring the niceties that are brought forth from both Flask
and Falcon and unify them into a single framework, along with some new ideas I have. I
also wanted to take some of the API primitives that are instilled in the Requests
library and put them into a web framework. So, you'll find a lot of parallels here with
Requests.
api = responder.API(lifespan=lifespan)
- Setting `resp.content` sends back bytes.
- Setting `resp.text` sends back unicode, while setting `resp.html` sends back HTML.
- Setting `resp.media` sends back JSON/YAML (`.text`/`.html`/`.content` override this).
- Case-insensitive `req.headers` dict (from Requests directly).
- `resp.status_code`, `req.method`, `req.url`, and other familiar friends.
# GraphQL
import graphene
api.graphql("/graphql", schema=graphene.Schema(query=Query))
## Ideas
# WebSockets
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s
new f-string syntax.
- I love Falcon's "every request and response is passed into to each view and mutated"
methodology, especially `response.media`, and have used it here. In addition to
supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly
taking over the world, and it uses YAML for all the things. Content-negotiation and
all that.
- **A built in testing client that uses the actual Requests you know and love**.
- The ability to mount other WSGI apps easily.
- Automatic gzipped-responses.
- In addition to Falcon's `on_get`, `on_post`, etc methods, Responder features an
`on_request` method, which gets called on every type of request, much like Requests.
- A production static file server is built-in.
- Uvicorn built-in as a production web server. I would have chosen Gunicorn, but it
doesn't run on Windows. Plus, Uvicorn serves well to protect against slowloris
attacks, making nginx unnecessary in production.
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at
any route, magically.
- Provide an official way to run webpack.
# Mount WSGI/ASGI apps
from flask import Flask
flask_app = Flask(__name__)
api.mount("/flask", flask_app)
## Development
# Background tasks
@api.route("/work")
def do_work(req, resp):
@api.background.task
def process():
import time; time.sleep(10)
process()
resp.media = {"status": "processing"}
```
See [Development Sandbox](DEVELOP.md).
Built-in OpenAPI docs, cookie-based sessions, gzip compression, static file serving, Jinja2 templates, and a production uvicorn server.
Route convertors: `str`, `int`, `float`, `uuid`, `path`.
## Documentation
https://responder.kennethreitz.org
-37
View File
@@ -1,37 +0,0 @@
# ruff: noqa: S605, S607
"""
Build and publish a .deb package.
https://pypi.python.org/pypi/stdeb/0.8.5#quickstart-2-just-tell-me-the-fastest-way-to-make-a-deb
"""
import os
from shutil import rmtree
here = os.path.abspath(os.path.dirname(__file__))
def get_version():
import responder
return responder.__version__
def run():
version = get_version()
try:
print("Removing previous builds")
rmtree(os.path.join(here, "deb_dist"))
except FileNotFoundError:
pass
print("Creating Debian package manifest")
os.system(
"python setup.py --command-packages=stdeb.command sdist_dsc "
"-z artful --package3=pipenv --depends3=python3-virtualenv-clone"
)
print("Building .deb")
os.chdir(f"deb_dist/pipenv-{version}")
os.system("dpkg-buildpackage -rfakeroot -uc -us")
if __name__ == "__main__":
run()
-6
View File
@@ -1,6 +0,0 @@
alabaster<0.8
jinja2<3.2
markupsafe<4
readme-renderer<45
sphinx>=5,<9
sphinxcontrib-websupport<2.1
-7
View File
@@ -1,7 +0,0 @@
/* Hide module name and default value for environment variable section */
div[id$="environment-variables"] code.descclassname {
display: none;
}
div[id$="environment-variables"] em.property {
display: none;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,19 +0,0 @@
/*
Copyright (C) 2011-2018 Hoefler & Co.
This software is the property of Hoefler & Co. (H&Co).
Your right to access and use this software is subject to the
applicable License Agreement, or Terms of Service, that exists
between you and H&Co. If no such agreement exists, you may not
access or use this software for any purpose.
This software may only be hosted at the locations specified in
the applicable License Agreement or Terms of Service, and only
for the purposes expressly set forth therein. You may not copy,
modify, convert, create derivative works from or distribute this
software in any way, or make it accessible to any third party,
without first obtaining the written permission of H&Co.
For more information, please visit us at http://typography.com.
148887-130097-20181011
*/
<!-- sorry your browser is not supported. -->
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,177 +0,0 @@
/*
Copyright (C) 2011-2018 Hoefler & Co.
This software is the property of Hoefler & Co. (H&Co).
Your right to access and use this software is subject to the
applicable License Agreement, or Terms of Service, that exists
between you and H&Co. If no such agreement exists, you may not
access or use this software for any purpose.
This software may only be hosted at the locations specified in
the applicable License Agreement or Terms of Service, and only
for the purposes expressly set forth therein. You may not copy,
modify, convert, create derivative works from or distribute this
software in any way, or make it accessible to any third party,
without first obtaining the written permission of H&Co.
For more information, please visit us at http://typography.com.
148887-130097-20181011
*/
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/AA83D0999C9464BC6.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/AA83D0999C9464BC6.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 4r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/AA83D0999C9464BC6.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/AA83D0999C9464BC6.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/005CED86771E6F899.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/005CED86771E6F899.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 4i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/005CED86771E6F899.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/005CED86771E6F899.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/40351B0A9DF3B9622.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/40351B0A9DF3B9622.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 6r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/40351B0A9DF3B9622.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/40351B0A9DF3B9622.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3A39878E22934F8AD.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3A39878E22934F8AD.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 6i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3A39878E22934F8AD.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3A39878E22934F8AD.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/DD7AD6D5FDE05ABCA.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/DD7AD6D5FDE05ABCA.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Mercury Text G1 7r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/DD7AD6D5FDE05ABCA.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/DD7AD6D5FDE05ABCA.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/777143010DB6642D4.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/777143010DB6642D4.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: "Mercury Text G1 7i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/777143010DB6642D4.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/777143010DB6642D4.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/4A801A74B6CEC6B76.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/4A801A74B6CEC6B76.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm 4r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/4A801A74B6CEC6B76.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/4A801A74B6CEC6B76.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7E9ADDBCA2C8BD433.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7E9ADDBCA2C8BD433.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm 4i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7E9ADDBCA2C8BD433.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7E9ADDBCA2C8BD433.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/13B223000FA5C8685.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/13B223000FA5C8685.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm 7r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/13B223000FA5C8685.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/13B223000FA5C8685.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/0AC552691F872135E.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/0AC552691F872135E.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm 7i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/0AC552691F872135E.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/0AC552691F872135E.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,177 +0,0 @@
/*
Copyright (C) 2011-2018 Hoefler & Co.
This software is the property of Hoefler & Co. (H&Co).
Your right to access and use this software is subject to the
applicable License Agreement, or Terms of Service, that exists
between you and H&Co. If no such agreement exists, you may not
access or use this software for any purpose.
This software may only be hosted at the locations specified in
the applicable License Agreement or Terms of Service, and only
for the purposes expressly set forth therein. You may not copy,
modify, convert, create derivative works from or distribute this
software in any way, or make it accessible to any third party,
without first obtaining the written permission of H&Co.
For more information, please visit us at http://typography.com.
148887-130097-20181011
*/
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7D0605C11BA3A93EF.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7D0605C11BA3A93EF.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 4r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7D0605C11BA3A93EF.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7D0605C11BA3A93EF.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/81A13EBFC10447CAC.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/81A13EBFC10447CAC.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 4i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/81A13EBFC10447CAC.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/81A13EBFC10447CAC.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/25E3F5D50DDE1C555.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/25E3F5D50DDE1C555.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 6r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/25E3F5D50DDE1C555.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/25E3F5D50DDE1C555.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/F526A6C670B9765E2.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/F526A6C670B9765E2.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 6i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/F526A6C670B9765E2.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/F526A6C670B9765E2.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 600;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7DCD4B5CCAEE3223E.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7DCD4B5CCAEE3223E.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Mercury Text G1 7r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7DCD4B5CCAEE3223E.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/7DCD4B5CCAEE3223E.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Mercury Text G1 A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C08332ABD7F145352.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C08332ABD7F145352.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: "Mercury Text G1 7i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C08332ABD7F145352.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C08332ABD7F145352.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C579DF5B35B145D49.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C579DF5B35B145D49.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm 4r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C579DF5B35B145D49.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/C579DF5B35B145D49.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/E3597D43523236FD8.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/E3597D43523236FD8.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm 4i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/E3597D43523236FD8.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/E3597D43523236FD8.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3CBFC66855DD9B6EA.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3CBFC66855DD9B6EA.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm 7r";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3CBFC66855DD9B6EA.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/3CBFC66855DD9B6EA.eot?#hco")
format("embedded-opentype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm A";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/09E151FC31ECEF374.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/09E151FC31ECEF374.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: "Operator Mono SSm 7i";
src: url("http://python-responder.org/en/latest/_static/fonts/692185/09E151FC31ECEF374.eot");
src: url("http://python-responder.org/en/latest/_static/fonts/692185/09E151FC31ECEF374.eot?#hco")
format("embedded-opentype");
font-style: italic;
font-weight: 700;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,19 +0,0 @@
/*
Copyright (C) 2011-2018 Hoefler & Co.
This software is the property of Hoefler & Co. (H&Co).
Your right to access and use this software is subject to the
applicable License Agreement, or Terms of Service, that exists
between you and H&Co. If no such agreement exists, you may not
access or use this software for any purpose.
This software may only be hosted at the locations specified in
the applicable License Agreement or Terms of Service, and only
for the purposes expressly set forth therein. You may not copy,
modify, convert, create derivative works from or distribute this
software in any way, or make it accessible to any third party,
without first obtaining the written permission of H&Co.
For more information, please visit us at http://typography.com.
148887-130097-20181011
*/
<!-- sorry your browser is not supported. -->
File diff suppressed because one or more lines are too long
-161
View File
@@ -1,161 +0,0 @@
/*
* Konami-JS ~
* :: Now with support for touch events and multiple instances for
* :: those situations that call for multiple easter eggs!
* Code: https://github.com/snaptortoise/konami-js
* Copyright (c) 2009 George Mandis (georgemandis.com, snaptortoise.com)
* Version: 1.6.2 (7/17/2018)
* Licensed under the MIT License (http://opensource.org/licenses/MIT)
* Tested in: Safari 4+, Google Chrome 4+, Firefox 3+, IE7+, Mobile Safari 2.2.1+ and Android
*/
var Konami = function (callback) {
var konami = {
addEvent: function (obj, type, fn, ref_obj) {
if (obj.addEventListener) obj.addEventListener(type, fn, false);
else if (obj.attachEvent) {
// IE
obj["e" + type + fn] = fn;
obj[type + fn] = function () {
obj["e" + type + fn](window.event, ref_obj);
};
obj.attachEvent("on" + type, obj[type + fn]);
}
},
removeEvent: function (obj, eventName, eventCallback) {
if (obj.removeEventListener) {
obj.removeEventListener(eventName, eventCallback);
} else if (obj.attachEvent) {
obj.detachEvent(eventName);
}
},
input: "",
pattern: "38384040373937396665",
keydownHandler: function (e, ref_obj) {
if (ref_obj) {
konami = ref_obj;
} // IE
konami.input += e ? e.keyCode : event.keyCode;
if (konami.input.length > konami.pattern.length) {
konami.input = konami.input.substr(konami.input.length - konami.pattern.length);
}
if (konami.input === konami.pattern) {
konami.code(konami._currentLink);
konami.input = "";
e.preventDefault();
return false;
}
},
load: function (link) {
this._currentLink = link;
this.addEvent(document, "keydown", this.keydownHandler, this);
this.iphone.load(link);
},
unload: function () {
this.removeEvent(document, "keydown", this.keydownHandler);
this.iphone.unload();
},
code: function (link) {
window.location = link;
},
iphone: {
start_x: 0,
start_y: 0,
stop_x: 0,
stop_y: 0,
tap: false,
capture: false,
orig_keys: "",
keys: [
"UP",
"UP",
"DOWN",
"DOWN",
"LEFT",
"RIGHT",
"LEFT",
"RIGHT",
"TAP",
"TAP",
],
input: [],
code: function (link) {
konami.code(link);
},
touchmoveHandler: function (e) {
if (e.touches.length === 1 && konami.iphone.capture === true) {
var touch = e.touches[0];
konami.iphone.stop_x = touch.pageX;
konami.iphone.stop_y = touch.pageY;
konami.iphone.tap = false;
konami.iphone.capture = false;
konami.iphone.check_direction();
}
},
touchendHandler: function () {
konami.iphone.input.push(konami.iphone.check_direction());
if (konami.iphone.input.length > konami.iphone.keys.length)
konami.iphone.input.shift();
if (konami.iphone.input.length === konami.iphone.keys.length) {
var match = true;
for (var i = 0; i < konami.iphone.keys.length; i++) {
if (konami.iphone.input[i] !== konami.iphone.keys[i]) {
match = false;
}
}
if (match) {
konami.iphone.code(konami._currentLink);
}
}
},
touchstartHandler: function (e) {
konami.iphone.start_x = e.changedTouches[0].pageX;
konami.iphone.start_y = e.changedTouches[0].pageY;
konami.iphone.tap = true;
konami.iphone.capture = true;
},
load: function (link) {
this.orig_keys = this.keys;
konami.addEvent(document, "touchmove", this.touchmoveHandler);
konami.addEvent(document, "touchend", this.touchendHandler, false);
konami.addEvent(document, "touchstart", this.touchstartHandler);
},
unload: function () {
konami.removeEvent(document, "touchmove", this.touchmoveHandler);
konami.removeEvent(document, "touchend", this.touchendHandler);
konami.removeEvent(document, "touchstart", this.touchstartHandler);
},
check_direction: function () {
x_magnitude = Math.abs(this.start_x - this.stop_x);
y_magnitude = Math.abs(this.start_y - this.stop_y);
x = this.start_x - this.stop_x < 0 ? "RIGHT" : "LEFT";
y = this.start_y - this.stop_y < 0 ? "DOWN" : "UP";
result = x_magnitude > y_magnitude ? x : y;
result = this.tap === true ? "TAP" : result;
return result;
},
},
};
typeof callback === "string" && konami.load(callback);
if (typeof callback === "function") {
konami.code = callback;
konami.load();
}
return konami;
};
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
module.exports = Konami;
} else {
if (typeof define === "function" && define.amd) {
define([], function () {
return Konami;
});
} else {
window.Konami = Konami;
}
}
+3 -222
View File
@@ -1,240 +1,21 @@
<link
rel="stylesheet"
type="text/css"
href="https://cloud.typography.com/7584432/7586812/css/fonts.css"
/>
<script type="text/javascript">
$("#searchbox").hide(0);
</script>
<!--Alabaster (krTheme++) Hacks -->
<!-- CSS Adjustments (I'm very picky.) -->
<style type="text/css">
/* Rezzy requires precise alignment. */
img.logo {
margin-left: -20px !important;
}
h1 {
font-family: "Mercury Text G1 A", "Mercury Text G1 B" !important;
font-style: normal !important;
font-weight: 600 !important;
}
.section {
font-family: "Mercury Text G1 A", "Mercury Text G1 B" !important;
font-style: normal !important;
font-weight: 400 !important;
}
pre,
.pre,
.class em,
.descname,
.method em {
font-family: "Operator Mono SSm A", "Operator Mono SSm B", monospace !important;
font-weight: 400 !important;
}
.property {
color: lightgrey !important;
}
.method .descname {
color: #220a54;
}
.method {
margin-bottom: 2em;
}
.si,
.s2,
.s1,
.method em,
.class em {
font-style: italic !important;
color: grey;
}
.method em,
.class em {
margin-left: 0.3em;
margin-right: 0.3em;
}
.method p,
.class p {
font-family: "Mercury Text G1 A", "Mercury Text G1 B";
font-style: italic !important;
font-weight: 400 !important;
font-size: 1.15em;
}
.method p:first,
.class p:first {
background: #fffcbf;
}
.class .property {
display: none;
}
#testimonials p.attribution {
margin-top: -1em;
}
/* "Quick Search" should be not be shown for now. */
div#searchbox h3 {
display: none;
}
/* Make the document a little wider, less code is cut-off. */
/* Make the document a little wider. */
div.document {
width: 1008px;
}
/* Much-improved spacing around code blocks. */
/* Better spacing around code blocks. */
div.highlight pre {
padding: 11px 14px;
}
/* Remain Responsive! */
/* Responsive layout. */
@media screen and (max-width: 1008px) {
div.sphinxsidebar {
display: none;
}
div.document {
width: 100% !important;
}
/* Have code blocks escape the document right-margin. */
div.highlight pre {
margin-right: -30px;
}
}
</style>
<!-- Analytics tracking for Kenneth. -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-127383416-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "UA-127383416-1");
</script>
<!-- There are no more hacks. -->
<!-- இڿڰۣ-ڰۣ— -->
<!-- Love, Kenneth Reitz -->
<script src="{{ pathto('_static/', 1) }}/konami.js"></script>
<script>
var easter_egg = new Konami(
"https://www.myfortunecookie.co.uk/fortunes/" +
(Math.floor(Math.random() * 152) + 1)
);
</script>
<style>
.injected {
display: none !important;
}
</style>
<!-- GitHub Logo -->
<a
href="https://github.com/kennethreitz/responder"
class="github-corner"
aria-label="View source on GitHub"
>
<svg
width="80"
height="80"
viewBox="0 0 250 250"
style="fill: #151513; color: #fff; position: absolute; top: 0; border: 0; right: 0"
aria-hidden="true"
>
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor"
style="transform-origin: 130px 106px"
class="octo-arm"
></path>
<path
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor"
class="octo-body"
></path>
</svg>
</a>
<style>
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0);
}
20%,
60% {
transform: rotate(-25deg);
}
40%,
80% {
transform: rotate(10deg);
}
}
@media (max-width: 500px) {
.github-corner:hover .octo-arm {
animation: none;
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
}
</style>
<!-- That was not a hack. That was art.
<!-- UserVoice JavaScript SDK (only needed once on a page) -->
<script>
(function () {
var uv = document.createElement("script");
uv.type = "text/javascript";
uv.async = true;
uv.src = "//widget.uservoice.com/f4AQraEfwInlMzkexfRLg.js";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(uv, s);
})();
</script>
<!-- A tab to launch the Classic Widget -->
<script>
UserVoice = window.UserVoice || [];
UserVoice.push([
"showTab",
"classic_widget",
{
mode: "feedback",
primary_color: "#fa8c28",
link_color: "#0a8cc6",
forum_id: 913660,
tab_label: "Got feedback?",
tab_color: "#00994f",
tab_position: "bottom-left",
tab_inverted: true,
},
]);
</script>
+5 -85
View File
@@ -1,94 +1,14 @@
<p class="logo">
<a href="{{ pathto(master_doc) }}">
<img
class="logo"
src="{{ pathto('_static/responder.png', 1) }}"
title="https://kennethreitz.org/tattoos"
/>
<img class="logo" src="{{ pathto('_static/responder.png', 1) }}" />
</a>
</p>
<p>
<iframe
src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
allowtransparency="true"
frameborder="0"
scrolling="0"
width="200px"
height="35px"
></iframe>
<strong>Responder</strong> — a familiar HTTP service framework for Python.
</p>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css"
/>
<style>
.algolia-autocomplete {
width: 100%;
height: 1.5em;
}
.algolia-autocomplete a {
border-bottom: none !important;
}
#doc_search {
width: 100%;
height: 100%;
}
</style>
<input id="doc_search" placeholder="Search the doc" autofocus />
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
onload="docsearch({
apiKey: 'ac965312db252e0496283c75c6f76f0b',
indexName: 'python-responder',
inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown
})"
async
></script>
<p><strong>Responder</strong> is a web service framework, written for human beings.</p>
<h3>Stay Informed</h3>
<p>Receive updates on new releases and upcoming projects.</p>
<p>
<iframe
src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true"
allowtransparency="true"
frameborder="0"
scrolling="0"
width="200"
height="20"
></iframe>
</p>
<p>
<a
href="https://twitter.com/kennethreitz"
class="twitter-follow-button"
data-show-count="false"
>Follow @kennethreitz</a
>
<script>
!(function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0],
p = /^http:/.test(d.location) ? "http" : "https";
if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = p + "://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
}
})(document, "script", "twitter-wjs");
</script>
</p>
<h3>Useful Links</h3>
<ul>
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="http://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
</ul>
-94
View File
@@ -1,94 +0,0 @@
<p class="logo">
<a href="{{ pathto(master_doc) }}">
<img
class="logo"
src="{{ pathto('_static/responder.png', 1) }}"
title="https://kennethreitz.org/tattoos"
/>
</a>
</p>
<p>
<iframe
src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
allowtransparency="true"
frameborder="0"
scrolling="0"
width="200px"
height="35px"
></iframe>
</p>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css"
/>
<style>
.algolia-autocomplete {
width: 100%;
height: 1.5em;
}
.algolia-autocomplete a {
border-bottom: none !important;
}
#doc_search {
width: 100%;
height: 100%;
}
</style>
<input id="doc_search" placeholder="Search the doc" autofocus />
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
onload="docsearch({
apiKey: 'ac965312db252e0496283c75c6f76f0b',
indexName: 'python-responder',
inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown
})"
async
></script>
<p><strong>Responder</strong> is a web service framework, written for human beings.</p>
<h3>Stay Informed</h3>
<p>Receive updates on new releases and upcoming projects.</p>
<p>
<iframe
src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true"
allowtransparency="true"
frameborder="0"
scrolling="0"
width="200"
height="20"
></iframe>
</p>
<p>
<a
href="https://twitter.com/kennethreitz"
class="twitter-follow-button"
data-show-count="false"
>Follow @kennethreitz</a
>
<script>
!(function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0],
p = /^http:/.test(d.location) ? "http" : "https";
if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = p + "://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
}
})(document, "script", "twitter-wjs");
</script>
</p>
<h3>Useful Links</h3>
<ul>
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="http://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
</ul>
+77 -13
View File
@@ -1,35 +1,99 @@
API Reference
=============
API Documentation
=================
This page documents Responder's public Python API. For usage examples
and explanations, see the :doc:`quickstart` and :doc:`tour`.
Web Service (API) Class
-----------------------
The API Class
-------------
The central object of every Responder application. It holds your routes,
middleware, templates, and configuration. Create one at the top of your
module and use it to define your entire web service.
.. module:: responder
.. autoclass:: API
:inherited-members:
Requests & Responses
--------------------
Request
-------
The request object is passed into every view as the first argument. It
gives you access to everything the client sent — headers, query
parameters, the request body, cookies, and more.
Most properties are synchronous, but reading the body requires ``await``
because it involves I/O.
.. autoclass:: Request
:inherited-members:
Response
--------
The response object is passed into every view as the second argument.
Mutate it to control what gets sent back to the client — the body,
status code, headers, and cookies.
.. autoclass:: Response
:inherited-members:
Utility Functions
-----------------
Route Groups
------------
.. autofunction:: responder.API.status_codes.is_100
Group related routes under a shared URL prefix — useful for API versioning
and organizing large applications.
.. autofunction:: responder.API.status_codes.is_200
.. autoclass:: responder.api.RouteGroup
:members:
.. autofunction:: responder.API.status_codes.is_300
.. autofunction:: responder.API.status_codes.is_400
Background Queue
----------------
.. autofunction:: responder.API.status_codes.is_500
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
-------------------
Convenience functions for checking which category a status code falls
into. Useful in middleware and after-request hooks.
.. autofunction:: responder.status_codes.is_100
.. autofunction:: responder.status_codes.is_200
.. autofunction:: responder.status_codes.is_300
.. autofunction:: responder.status_codes.is_400
.. autofunction:: responder.status_codes.is_500
+7
View File
@@ -0,0 +1,7 @@
# Backlog
## Future Ideas
- Consider adding `after_request` hooks (complement to `before_request`)
- Explore WebSocket before_request short-circuit support
- Add rate limiting middleware
- Consider async template rendering by default
+1
View File
@@ -0,0 +1 @@
../../CHANGELOG.md
+81
View File
@@ -0,0 +1,81 @@
Command Line Interface
======================
Responder installs a ``responder`` command that lets you launch
applications from the terminal. You can point it at a Python module,
a local file, or even a URL — and it will find your ``API`` instance
and start serving.
Launching from a Module
-----------------------
The most common way to run a Responder application in production. Use
Python's standard dotted module path::
$ responder run acme.app
This imports ``acme.app`` and looks for an attribute called ``api``
(a ``responder.API`` instance). It's the same import system Python
uses everywhere — your ``PYTHONPATH`` and virtual environment are
respected.
Launching from a File
---------------------
During development, you often have a single file you want to run::
$ responder run helloworld.py
This loads the file directly and starts the server. Quick and easy for
prototyping and single-file applications.
You can test it with a simple HTTP request::
$ curl http://127.0.0.1:5042/hello
hello, world!
Launching from a URL
--------------------
Responder can fetch and run a Python file from any URL — great for
demos, sharing examples, and running code from GitHub::
$ responder run https://github.com/kennethreitz/responder/raw/refs/heads/main/examples/helloworld.py
This also works with ``github://`` URLs and any filesystem protocol
supported by `fsspec <https://filesystem-spec.readthedocs.io/>`_::
$ responder run github://kennethreitz:responder@/examples/helloworld.py
Cloud storage is supported too — Azure Blob Storage, Google Cloud
Storage, S3, HDFS, SFTP, and more. Install ``fsspec[full]`` for all
protocols::
$ uv pip install 'fsspec[full]'
Custom Instance Names
---------------------
By default, Responder looks for an attribute called ``api``. If your
application uses a different name, specify it with a colon::
$ responder run acme.app:service
$ responder run myapp.py:application
For URLs, use a fragment::
$ responder run https://example.com/app.py#service
Building Frontend Assets
-------------------------
If your project includes a JavaScript frontend with a ``package.json``,
the ``build`` subcommand runs ``npm run build``::
$ responder build
$ responder build /path/to/frontend
+23 -181
View File
@@ -1,91 +1,35 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# Sphinx configuration for Responder documentation.
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
import os
project = "responder"
copyright = "2024, A Kenneth Reitz project"
copyright = "2018-2026, Kenneth Reitz"
author = "Kenneth Reitz"
# The short X.Y version
version = ""
here = os.path.abspath(os.path.dirname(__file__))
about = {}
with open(os.path.join(here, "..", "..", "responder", "__version__.py")) as f:
exec(f.read(), about)
# -- General configuration ---------------------------------------------------
version = about["__version__"]
release = about["__version__"]
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.doctest",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.coverage",
"sphinx.ext.mathjax",
"sphinx.ext.ifconfig",
"sphinx.ext.viewcode",
"sphinx.ext.githubpages",
"myst_parser",
"sphinx_copybutton",
"sphinx_design_elements",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
# The master toctree document.
source_suffix = {".rst": "restructuredtext"}
master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = "en"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
# Theme
html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
"show_powered_by": False,
"github_user": "kennethreitz",
@@ -93,118 +37,16 @@ html_theme_options = {
"github_banner": False,
"show_related": False,
}
html_sidebars = {
"index": ["sidebarintro.html", "sourcelink.html", "searchbox.html", "hacks.html"],
"**": [
"sidebarlogo.html",
"localtoc.html",
"relations.html",
"sourcelink.html",
"searchbox.html",
"hacks.html",
],
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = "responderdoc"
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
html_sidebars = {
"index": ["sidebarintro.html", "searchbox.html"],
"**": ["sidebarintro.html", "localtoc.html", "searchbox.html"],
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, "responder.tex", "responder Documentation", "Kenneth Reitz", "manual")
]
# MyST
myst_heading_anchors = 3
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "responder", "responder Documentation", [author], 1)]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"responder",
"responder Documentation",
author,
"responder",
"One line description of project.",
"Miscellaneous",
)
]
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''
# A unique identification for the text.
#
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ["search.html"]
# -- Extension configuration -------------------------------------------------
# -- Options for intersphinx extension ---------------------------------------
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"https://docs.python.org/": None}
# -- Options for todo extension ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# Copybutton
copybutton_remove_prompts = True
copybutton_prompt_text = r">>> |\.\.\. |\$ "
copybutton_prompt_is_regexp = True
+92 -47
View File
@@ -1,58 +1,103 @@
Deploying Responder
===================
Deployment
==========
You can deploy Responder anywhere you can deploy a basic Python application.
Responder applications are standard `ASGI <https://asgi.readthedocs.io/>`_
apps. ASGI (Asynchronous Server Gateway Interface) is the modern successor
to WSGI — it supports async, WebSockets, and HTTP/2. This means you can
deploy a Responder app anywhere that runs Python, using any ASGI server.
Docker Deployment
-----------------
Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
Running Locally
---------------
``Dockerfile``::
FROM kennethreitz/pipenv
ENV PORT '80'
COPY . /app
CMD python3 api.py
EXPOSE 80
That's it!
Heroku Deployment
-----------------
The basics::
$ mkdir my-api
$ cd my-api
$ git init
$ heroku create
...
Install Responder::
$ pipenv install responder
...
Write out an ``api.py``::
import responder
api = responder.API()
@api.route("/")
async def hello(req, resp):
resp.text = "hello, world!"
During development, ``api.run()`` is all you need::
if __name__ == "__main__":
api.run()
Write out a ``Procfile``::
This starts a `uvicorn <https://www.uvicorn.org/>`_ server on
``127.0.0.1:5042``. Uvicorn is a lightning-fast ASGI server built on
`uvloop <https://uvloop.readthedocs.io/>`_ — it handles thousands of
concurrent connections efficiently and protects against slowloris attacks,
making a reverse proxy like nginx optional for many deployments.
web: python api.py
That's it! Next, we commit and push to Heroku::
Docker
------
$ git add -A
$ git commit -m 'initial commit'
$ git push heroku master
Docker is the most common way to package and deploy web applications.
Here's a minimal Dockerfile::
FROM python:3.13-slim
WORKDIR /app
COPY . .
RUN pip install responder
ENV PORT=80
EXPOSE 80
CMD ["python", "api.py"]
Build and run::
$ docker build -t myapi .
$ docker run -p 8000:80 myapi
The ``python:3.13-slim`` image is about 150MB — small enough for fast
deploys but includes everything you need. For even smaller images, you
can use ``python:3.13-alpine``, though some packages may need extra
build dependencies.
Cloud Platforms
---------------
Responder automatically honors the ``PORT`` environment variable. When
``PORT`` is set, the server binds to ``0.0.0.0`` on that port — this is
the convention that virtually every cloud platform uses.
This means zero configuration on:
- **Fly.io**``fly launch`` and you're done
- **Railway** — push your code, Railway sets ``PORT``
- **Render** — set start command to ``python api.py``
- **Google Cloud Run** — containerize and deploy
- **Azure Container Apps** — same pattern
- **AWS App Runner** — and here too
The pattern is always the same: deploy your code, set the start command
to ``python api.py``, and the platform handles the rest.
Uvicorn Directly
----------------
For production deployments where you want more control, bypass
``api.run()`` and use uvicorn directly::
$ uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4
The ``--workers`` flag spawns multiple processes, each handling requests
independently. A good starting point is 2-4 workers per CPU core.
Uvicorn supports many options — SSL certificates, access logging, graceful
shutdown timeouts, and more. See the
`uvicorn documentation <https://www.uvicorn.org/deployment/>`_ for details.
Reverse Proxy
-------------
For high-traffic production deployments, you may want a reverse proxy like
`nginx <https://nginx.org/>`_ or `Caddy <https://caddyserver.com/>`_ in
front of your application for:
- **SSL/TLS termination** — let the proxy handle HTTPS certificates
- **Load balancing** — distribute traffic across multiple app instances
- **Static asset serving** — offload static files to the proxy
- **Rate limiting** — at the infrastructure level
Responder's ``TrustedHostMiddleware`` and ``HTTPSRedirectMiddleware`` work
correctly behind proxies that set standard forwarding headers
(``X-Forwarded-For``, ``X-Forwarded-Proto``).
That said, uvicorn is production-ready on its own. Many applications run
uvicorn directly without a reverse proxy and do just fine.
+172
View File
@@ -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).
+90 -102
View File
@@ -1,25 +1,7 @@
.. responder documentation master file, created by
sphinx-quickstart on Thu Oct 11 12:58:34 2018.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Responder
=========
A familiar HTTP Service Framework
=================================
|Build Status| |image1| |image2| |image3| |image4| |image5|
.. |Build Status| image:: https://github.com/kennethreitz/responder/actions/workflows/test.yaml/badge.svg
:target: https://github.com/kennethreitz/responder/actions/workflows/test.yaml
.. |image1| image:: https://img.shields.io/pypi/v/responder.svg
:target: https://pypi.org/project/responder/
.. |image2| image:: https://img.shields.io/pypi/l/responder.svg
:target: https://pypi.org/project/responder/
.. |image3| image:: https://img.shields.io/pypi/pyversions/responder.svg
:target: https://pypi.org/project/responder/
.. |image4| image:: https://img.shields.io/github/contributors/kennethreitz/responder.svg
:target: https://github.com/kennethreitz/responder/graphs/contributors
.. |image5| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg
:target: https://saythanks.io/to/kennethreitz
A familiar HTTP Service Framework for Python.
.. code:: python
@@ -34,109 +16,115 @@ A familiar HTTP Service Framework
if __name__ == '__main__':
api.run()
Powered by `Starlette <https://www.starlette.io/>`_. That ``async`` declaration is optional.
Powered by `Starlette`_, `uvicorn`_, and good intentions. The ``async`` is optional.
This gets you a ASGI app, with a production static files server
(`WhiteNoise <http://whitenoise.evans.io/en/stable/>`_)
pre-installed, jinja2 templating (without additional imports), and a
production webserver based on uvloop, serving up requests with
automatic gzip compression.
Features
The Idea
--------
- A pleasant API, with a single import statement.
- Class-based views without inheritance.
- `ASGI <https://asgi.readthedocs.io>`_ framework, the future of Python web services.
- WebSocket support!
- The ability to mount any ASGI / WSGI app at a subroute.
- `f-string syntax <https://docs.python.org/3/whatsnew/3.6.html#pep-498-formatted-string-literals>`_ route declaration.
- Mutable response object, passed into each view. No need to return anything.
- Background tasks, spawned off in a ``ThreadPoolExecutor``.
- GraphQL (with *GraphiQL*) support!
- OpenAPI schema generation, with interactive documentation!
- Single-page webapp support!
If you've ever used `Flask`_, the routing will look familiar. If you've
used `Falcon`_, the request/response pattern will click immediately. And
if you've used `Requests`_ — well, you'll feel right at home.
Testimonials
Responder takes these ideas and brings them together. Every view receives
a request and a response. You read from one and write to the other. No
return values, no special response classes, no boilerplate.
- ``resp.text`` sends text. ``resp.html`` sends HTML. ``resp.media`` sends JSON.
- ``resp.file("path")`` serves a file. ``resp.content`` sends raw bytes.
- ``req.headers`` is case-insensitive. ``req.params`` holds query parameters.
- ``resp.status_code``, ``req.method``, ``req.url`` — the familiar ones.
Set ``resp.media`` to a dict and the right thing happens. If the client
asks for YAML, it gets YAML. Content negotiation is automatic.
Responder and `FastAPI`_ are siblings — both built on Starlette, both
born around the same time, both part of the push that made ASGI the
future of Python web services. FastAPI went deep on type annotations
and automatic validation. Responder went for simplicity and a mutable
request/response pattern. Both projects are better for the other
existing. Use whichever feels right.
This is a passion project. It exists because building a web framework
from scratch is one of the best ways to understand how the web works.
It's a great fit for personal projects, prototyping, teaching, research,
and anyone who values a clean API over a sprawling ecosystem. If you
need battle-tested infrastructure at scale, FastAPI and Django will
serve you well. If you want something small, expressive, and fun to
work with — welcome.
What You Get
------------
“Pleasantly very taken with python-responder.
`@kennethreitz <https://twitter.com/kennethreitz>`_ at his absolute
best.”
One ``pip install``, batteries included:
—Rudraksh M.K.
- Mount Flask, Django, or any WSGI/ASGI app at a subroute.
- Gzip compression, HSTS, CORS, and trusted host validation.
- Before-request hooks that can short-circuit for auth guards.
- A test client for fast, in-process testing with pytest.
- Route parameters with f-string syntax and type convertors.
- Lifespan context managers for startup and shutdown logic.
- Custom exception handlers for clean error responses.
- `GraphQL`_ with Graphene and a built-in GraphiQL IDE.
- File serving with automatic content-type detection.
- Sync and async views — ``async`` is always optional.
- Class-based views with ``on_get``, ``on_post``, ``on_request``.
- A pleasant API with a single import statement.
- OpenAPI schema generation with Swagger UI.
- A production `uvicorn`_ server, ready to deploy.
- HTTP method filtering for REST APIs.
- Signed cookie-based sessions.
- Background tasks in a thread pool.
- WebSocket support.
Installation
------------
..
.. code-block:: shell
"ASGI is going to enable all sorts of new high-performance web services. It's awesome to see Responder starting to take advantage of that."
$ uv pip install responder
—Tom Christie, author of `Django REST Framework`_
Python 3.10 and above. That's it.
..
“I love that you are exploring new patterns. Go go go!”
— Danny Greenfield, author of `Two Scoops of Django`_
.. _Django REST Framework: https://www.django-rest-framework.org/
.. _Two Scoops of Django: https://www.feldroy.com/two-scoops-press#two-scoops-of-django
User Guides
-----------
.. toctree::
:maxdepth: 2
:caption: User Guide
quickstart
tour
deployment
testing
api
cli
.. toctree::
:maxdepth: 2
:caption: Tutorials
tutorial-rest
tutorial-sqlalchemy
tutorial-auth
tutorial-websockets
tutorial-middleware
tutorial-flask
guide-config
.. toctree::
:maxdepth: 1
:caption: Project
changes
Sandbox <sandbox>
backlog
Installing Responder
--------------------
.. code-block:: shell
$ pipenv install responder
✨🍰✨
Only **Python 3.6+** is supported.
The Basic Idea
--------------
The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
- Setting ``resp.content`` sends back bytes.
- Setting ``resp.text`` sends back unicode, while setting ``resp.html`` sends back HTML.
- Setting ``resp.media`` sends back JSON/YAML (``.text``/``.html``/``.content`` override this).
- Case-insensitive ``req.headers`` dict (from Requests directly).
- ``resp.status_code``, ``req.method``, ``req.url``, and other familiar friends.
Ideas
-----
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s new f-string syntax.
- I love Falcon's "every request and response is passed into each view and mutated" methodology, especially ``response.media``, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
- **A built in testing client that uses the actual Requests you know and love**.
- The ability to mount other WSGI apps easily.
- Automatic gzipped-responses.
- In addition to Falcon's ``on_get``, ``on_post``, etc methods, Responder features an ``on_request`` method, which gets called on every type of request, much like Requests.
- A production static files server is built-in.
- `Uvicorn <https://www.uvicorn.org/>`_ is built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Uvicorn serves well to protect against `slowloris <https://en.wikipedia.org/wiki/Slowloris_(computer_security)>`_ attacks, making nginx unnecessary in production.
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. _Starlette: https://www.starlette.io/
.. _uvicorn: https://www.uvicorn.org/
.. _Flask: https://flask.palletsprojects.com/
.. _Falcon: https://falconframework.org/
.. _FastAPI: https://fastapi.tiangolo.com/
.. _GraphQL: https://graphql.org/
.. _Requests: https://requests.readthedocs.io/
+300 -99
View File
@@ -1,177 +1,378 @@
Quick Start!
============
Quick Start
===========
This section of the documentation exists to provide an introduction to the Responder interface,
as well as educate the user on basic functionality.
This guide will walk you through the basics of building a web service with
Responder. By the end, you'll understand how HTTP requests and responses
work, how to define routes, read data from clients, send data back, render
HTML templates, and process work in the background.
Declare a Web Service
---------------------
Create a Web Service
--------------------
The first thing you need to do is declare a web service::
Every web application starts with a single object — the application
instance. In Responder, this is the ``API`` class. It holds your routes,
middleware, templates, and configuration. Think of it as the central
nervous system of your web service::
import responder
api = responder.API()
Hello World!
------------
That's it. One import, one line. You now have a fully functional ASGI
application with gzip compression, static file serving, session support,
and a production-ready server — all wired up and ready to go.
Then, you can add a view / route to it.
Here, we'll make the root URL say "hello world!"::
Hello World
-----------
A web service isn't very useful until it can respond to requests. In HTTP,
a *route* maps a URL path to a function that handles it. When a client
(like a browser or ``curl``) sends a request to that path, your function
runs and produces a response.
Here's the simplest possible route::
@api.route("/")
def hello_world(req, resp):
resp.text = "hello, world!"
Two things to notice:
1. Every view function receives two arguments: ``req`` (the incoming
request) and ``resp`` (the outgoing response).
2. You don't return anything. Instead, you *mutate* the response object
directly. This is a deliberate design choice — it keeps the API
consistent whether you're setting text, JSON, headers, cookies, or
status codes.
Run the Server
--------------
Next, we can run our web service easily, with ``api.run()``::
Start your web service with a single call::
api.run()
This will spin up a production web server on port ``5042``, ready for incoming HTTP requests.
This spins up a production-grade `uvicorn <https://www.uvicorn.org/>`_
server on port ``5042``, ready for incoming HTTP requests. Open
``http://localhost:5042`` in your browser and you'll see your hello world
response.
Note: you can pass ``port=5000`` if you want to customize the port. The ``PORT`` environment variable for established web service providers (e.g. Heroku) will automatically be honored and will set the listening address to ``0.0.0.0`` automatically (also configurable through the ``address`` keyword argument).
You can customize the port with ``api.run(port=8000)``. The ``PORT``
environment variable is also honored automatically — when set, Responder
binds to ``0.0.0.0`` on that port, which is what cloud platforms expect.
.. note::
Both sync and async views are supported. The ``async`` keyword is always
optional — use it when you need to ``await`` something, like reading a
request body or querying a database.
Accept Route Arguments
----------------------
Route Parameters
----------------
If you want dynamic URLs, you can use Python's familiar *f-string syntax* to declare variables in your routes::
Static URLs like ``/about`` are useful, but most applications need dynamic
routes — URLs that contain variable data, like a user ID or a product slug.
In Responder, you declare route parameters using Python's f-string syntax::
@api.route("/hello/{who}")
def hello_to(req, resp, *, who):
resp.text = f"hello, {who}!"
A ``GET`` request to ``/hello/brettcannon`` will result in a response of ``hello, brettcannon!``.
A ``GET`` request to ``/hello/world`` will respond with ``hello, world!``.
A request to ``/hello/guido`` will respond with ``hello, guido!``.
Type convertors are also available::
Route parameters are passed as *keyword-only* arguments (after the ``*``
in the function signature). This is a Python feature that makes the
interface explicit — you always know which arguments come from the URL.
Type Convertors
^^^^^^^^^^^^^^^
By default, route parameters are strings. But often you want them as
integers, UUIDs, or other types. Responder can convert them automatically
using type annotations in the route pattern::
@api.route("/add/{a:int}/{b:int}")
async def add(req, resp, *, a, b):
resp.text = f"{a} + {b} = {a + b}"
Supported types: ``str``, ``int`` and ``float``.
Here, ``a`` and ``b`` will arrive as Python ``int`` objects, not strings.
If someone requests ``/add/3/hello``, they'll get a 404 — the route won't
match because ``hello`` isn't a valid integer.
Returning JSON / YAML
---------------------
Supported types:
If you want your API to send back JSON, simply set the ``resp.media`` property to a JSON-serializable Python object::
- ``str`` — matches any string without slashes (this is the default)
- ``int`` — matches digits and converts to ``int``
- ``float`` — matches decimal numbers and converts to ``float``
- ``uuid`` — matches UUID strings like ``550e8400-e29b-41d4-a716-446655440000``
- ``path`` — matches any string *including* slashes, useful for file paths
like ``/files/{filepath:path}``
Sending Responses
-----------------
When an HTTP server receives a request, it must send back a response. Every
HTTP response has three parts: a status code (like ``200 OK`` or ``404 Not
Found``), headers (metadata like ``Content-Type``), and a body (the actual
data).
Responder lets you set all three by mutating the response object.
**Text and HTML** — the simplest response types. ``resp.text`` sets the
``Content-Type`` to ``text/plain``, while ``resp.html`` sets it to
``text/html``::
resp.text = "plain text response"
resp.html = "<h1>HTML response</h1>"
**JSON** — the lingua franca of web APIs. Set ``resp.media`` to any
JSON-serializable Python object — a dict, a list, whatever — and Responder
will serialize it to JSON and set the right headers::
@api.route("/hello/{who}/json")
def hello_to(req, resp, *, who):
def hello_json(req, resp, *, who):
resp.media = {"hello": who}
A ``GET`` request to ``/hello/guido/json`` will result in a response of ``{'hello': 'guido'}``.
If the client sends an ``Accept: application/x-yaml`` header, the same data
will be returned as YAML instead. This is called *content negotiation*
the server and client agree on a format. It happens automatically.
If the client requests YAML instead (with a header of ``Accept: application/x-yaml``), YAML will be sent.
**Files** — serve a file from disk. Responder uses Python's ``mimetypes``
module to figure out the ``Content-Type`` from the file extension::
Rendering a Template
--------------------
resp.file("reports/annual.pdf")
Responder provides a built-in light `jinja2 <http://jinja.pocoo.org/docs/>`_ wrapper ``templates.Templates``
**Raw bytes** — for binary data like images or protocol buffers::
Usage::
resp.content = b"\x89PNG\r\n..."
from responder.templates import Templates
**Status codes** — HTTP status codes tell the client what happened. ``200``
means success, ``201`` means something was created, ``404`` means not found,
``500`` means the server broke. Set it directly::
templates = Templates()
resp.status_code = 201
@api.route("/hello/{name}/html")
def hello(req, resp, name):
resp.html = templates.render("hello.html", name=name)
**Headers** — HTTP headers carry metadata. Common ones include
``Content-Type``, ``Cache-Control``, ``Authorization``, and custom
application headers::
resp.headers["X-Custom"] = "value"
**Redirects** — tell the client to go somewhere else::
api.redirect(resp, location="/new-url")
This sends a ``301 Moved Permanently`` response by default. The client's
browser will automatically follow the redirect.
Also a ``render_async`` is available::
Reading Requests
----------------
templates = Templates(enable_async=True)
resp.html = await templates.render_async("hello.html", who=who)
The other half of HTTP is the request — the data the client sends to your
server. This includes the HTTP method (GET, POST, PUT, DELETE), the URL,
headers, query parameters, cookies, and optionally a body.
You can also use the existing ``api.template(filename, *args, **kwargs)`` to render templates::
Responder wraps all of this in the ``req`` object.
@api.route("/hello/{who}/html")
def hello_html(req, resp, *, who):
resp.html = api.template('hello.html', who=who)
**Method and URL** — every HTTP request has a method (what the client wants
to do) and a URL (what resource it's about)::
req.method # "get", "post", etc. (lowercase)
req.full_url # "http://example.com/path?q=1"
req.url # parsed URL object
**Headers** — HTTP headers carry metadata from the client, like what
content types it accepts, authentication tokens, and more. Responder's
headers dict is case-insensitive, because the HTTP spec says header names
are case-insensitive::
req.headers["Content-Type"]
req.headers["content-type"] # same thing
**Query parameters** — the part of the URL after the ``?``. These are
commonly used for search, filtering, and pagination::
# GET /search?q=python&page=2
req.params["q"] # "python"
req.params["page"] # "2"
Note that query parameters are always strings. If you need an integer,
you'll need to convert it yourself: ``int(req.params["page"])``.
**Path parameters** — the dynamic parts of the URL that matched your route
pattern. These are also available on the request object, which is useful
in before-request hooks where they aren't passed as function arguments::
req.path_params["user_id"] # same as the keyword argument
**Request body** — for POST, PUT, and PATCH requests, the client sends
data in the body. Since reading the body is an I/O operation, you need to
``await`` it::
# JSON body (the most common format for APIs)
data = await req.media()
# Form data (from HTML forms)
data = await req.media("form")
# File uploads (multipart)
files = await req.media("files")
# Raw bytes
body = await req.content
# Raw text
text = await req.text
**Other useful properties**::
req.is_json # True if the content type is JSON
req.cookies # dict of cookies sent by the client
req.session # session data (a signed, server-side dict)
req.client # (host, port) tuple — the client's IP address
req.is_secure # True if the request came over HTTPS
Setting Response Status Code
----------------------------
Rendering Templates
-------------------
If you want to set the response status code, simply set ``resp.status_code``::
While APIs typically return JSON, many web applications need to render
HTML pages. Responder includes built-in support for
`Jinja2 <https://jinja.palletsprojects.com/>`_, one of the most popular
templating engines in the Python ecosystem.
@api.route("/416")
def teapot(req, resp):
resp.status_code = api.status_codes.HTTP_416 # ...or 416
Templates let you write HTML with placeholders that get filled in with
dynamic data. This keeps your presentation logic (HTML) separate from
your application logic (Python) — a pattern called
*separation of concerns*.
The simplest way to render a template is ``api.template()``. Templates
are loaded from the ``templates/`` directory by default::
@api.route("/hello/{name}/html")
def hello_html(req, resp, *, name):
resp.html = api.template("hello.html", name=name)
The template file ``templates/hello.html`` might look like::
<h1>Hello, {{ name }}!</h1>
The ``{{ name }}`` part is a Jinja2 expression — it gets replaced with
the value you passed in.
You can also use the ``Templates`` class directly for more control over
the template directory and configuration::
from responder.templates import Templates
templates = Templates(directory="my_templates")
@api.route("/page")
def page(req, resp):
resp.html = templates.render("page.html", title="Hello")
For applications that need non-blocking template rendering (rare, but
useful under extreme load), async rendering is supported::
templates = Templates(directory="templates", enable_async=True)
resp.html = await templates.render_async("page.html", title="Hello")
And for quick one-off templates, you can render a string directly without
a file::
resp.html = api.template_string("Hello, {{ name }}!", name="world")
Setting Response Headers
------------------------
Background Tasks
----------------
If you want to set a response header, like ``X-Pizza: 42``, simply modify the ``resp.headers`` dictionary::
Sometimes you want to accept a request, respond immediately, and do the
actual processing later. This is a common pattern for operations that take
a long time — sending emails, processing images, updating caches, or
calling slow external APIs.
@api.route("/pizza")
def pizza_pizza(req, resp):
resp.headers['X-Pizza'] = '42'
That's it!
Receiving Data & Background Tasks
---------------------------------
If you're expecting to read any request data, on the server, you need to declare your view as async and await the content.
Here, we'll process our data in the background, while responding immediately to the client::
import time
Responder makes this easy with background tasks. Decorate any function
with ``@api.background.task`` and it will run in a thread pool, separate
from the request/response cycle::
@api.route("/incoming")
async def receive_incoming(req, resp):
@api.background.task
def process_data(data):
"""Just sleeps for three seconds, as a demo."""
time.sleep(3)
# Parse the incoming data as form-encoded.
# Note: 'json' and 'yaml' formats are also automatically supported.
data = await req.media()
# Process the data (in the background).
process_data(data)
# Immediately respond that upload was successful.
resp.media = {'success': True}
A ``POST`` request to ``/incoming`` will result in an immediate response of ``{'success': true}``.
Here's a sample code to post a file with background::
@api.route("/")
async def upload_file(req, resp):
@api.background.task
def process_data(data):
f = open('./{}'.format(data['file']['filename']), 'w')
f.write(data['file']['content'].decode('utf-8'))
f.close()
"""This runs in a background thread."""
import time
time.sleep(10) # simulate heavy work
data = await req.media(format='files')
process_data(data)
resp.media = {'success': 'ok'}
# This response is sent immediately, while process_data
# continues running in the background.
resp.media = {"status": "accepted"}
You can send a file easily with requests::
The client gets an instant response — the heavy lifting happens after.
This is the same pattern used by task queues like Celery, but much simpler
for lightweight use cases where you don't need a full message broker.
import requests
.. note::
data = {'file': ('hello.txt', 'hello, world!', "text/plain")}
r = requests.post('http://127.0.0.1:8210/file', files=data)
Background tasks run in threads, not processes. They share memory with
your application, which makes them fast to start but means CPU-intensive
work will block the event loop. For heavy computation, consider a proper
task queue.
print(r.text)
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
+36
View File
@@ -0,0 +1,36 @@
(sandbox)=
# Development Sandbox
## Setup
Set up a development sandbox.
Acquire sources and create virtualenv.
```shell
git clone https://github.com/kennethreitz/responder.git
cd responder
uv venv
```
Install project in editable mode, including
all development tools.
```shell
uv pip install --upgrade --editable '.[develop,docs,release,test]'
```
## Operations
Run tests.
```shell
source .venv/bin/activate
pytest
```
Format code.
```shell
ruff format .
ruff check --fix .
```
Documentation authoring.
```shell
sphinx-autobuild --open-browser --watch docs/source docs/source docs/build
```
+245 -40
View File
@@ -1,54 +1,53 @@
Building and Testing with Responder
===================================
Testing
=======
Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
Responder includes a built-in test client powered by Starlette's
``TestClient``. You don't need to start a server — tests run in-process,
making them fast and reliable. There's no separate test server to manage,
no ports to allocate, and no race conditions to worry about. Just import
your app and start making requests.
Here, we'll go over the basics of setting up a proper Python package and adding testing to it.
The Basics
----------
Getting Started
---------------
Your repository should look like this::
Pipfile Pipfile.lock api.py test_api.py
``$ cat api.py``::
Given a simple application in ``api.py``::
import responder
api = responder.API()
@api.route("/")
def hello_world(req, resp):
def hello(req, resp):
resp.text = "hello, world!"
if __name__ == "__main__":
api.run()
You can test it with pytest. Every Responder ``API`` instance has a
``requests`` property that gives you a test client — use it exactly like
you'd use ``requests`` or ``httpx``::
``$ cat Pipfile``::
# test_api.py
import api as service
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
def test_hello():
r = service.api.requests.get("/")
assert r.text == "hello, world!"
[packages]
responder = "*"
Run your tests::
[dev-packages]
pytest = "*"
$ pytest
[requires]
python_version = "3.7"
That's really all there is to it. No configuration, no test server setup.
[pipenv]
allow_prereleases = true
Writing Tests
-------------
Using Fixtures
--------------
``$ cat test_api.py``::
For larger test suites, pytest fixtures keep things organized. Create a
fixture that returns your API instance, and every test gets a fresh
reference to it::
import pytest
import api as service
@@ -57,25 +56,231 @@ Writing Tests
def api():
return service.api
def test_hello_world(api):
def test_hello(api):
r = api.requests.get("/")
assert r.text == "hello, world!"
``$ pytest``::
def test_json(api):
@api.route("/data")
def data(req, resp):
resp.media = {"key": "value"}
...
========================== 1 passed in 0.10 seconds ==========================
r = api.requests.get(api.url_for(data))
assert r.json() == {"key": "value"}
The ``api.url_for()`` method generates a URL for a given route endpoint,
so you don't have to hard-code paths in your tests. If you rename a route
later, your tests won't break.
(Optional) Proper Python Package
--------------------------------
Testing JSON APIs
-----------------
Optionally, you can not rely on relative imports, and instead install your api as a proper package. This requires:
Most APIs send and receive JSON. The test client makes this natural — pass
``json=`` to send a JSON body, and call ``.json()`` on the response to
parse it::
1. A `proper setup.py <https://github.com/kennethreitz/setup.py>`_ file.
2. ``$ pipenv install -e . --dev``
def test_create_item(api):
@api.route("/items")
async def create(req, resp):
data = await req.media()
resp.media = {"created": data}
resp.status_code = 201
This will allow you to only specify your dependencies once: in ``setup.py``. ``$ pipenv lock`` will automatically lock your transitive dependencies (e.g. Responder), even if it's not specified in the ``Pipfile``.
r = api.requests.post(api.url_for(create), json={"name": "widget"})
assert r.status_code == 201
assert r.json() == {"created": {"name": "widget"}}
This will ensure that your application gets installed in every developer's environment, using Pipenv.
You can also test content negotiation by setting the ``Accept`` header::
r = api.requests.get("/data", headers={"Accept": "application/x-yaml"})
assert "key: value" in r.text
Testing Request Validation
--------------------------
If you're using Pydantic models for request validation, you can test
that invalid inputs are properly rejected::
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
def test_validation(api):
@api.route("/items", methods=["POST"], request_model=Item)
async def create(req, resp):
data = await req.media()
resp.media = data
# Valid request
r = api.requests.post("/items", json={"name": "thing", "price": 9.99})
assert r.status_code == 200
# Missing required field
r = api.requests.post("/items", json={"name": "thing"})
assert r.status_code == 422
assert "errors" in r.json()
Testing File Uploads
--------------------
File uploads use the ``files`` parameter, just like the ``requests``
library. Each file is a tuple of ``(filename, content, content_type)``::
def test_upload(api):
@api.route("/upload")
async def upload(req, resp):
files = await req.media("files")
resp.media = {"received": list(files.keys())}
files = {"doc": ("report.pdf", b"content", "application/pdf")}
r = api.requests.post(api.url_for(upload), files=files)
assert r.json() == {"received": ["doc"]}
Testing Headers and Cookies
----------------------------
Check response headers and cookies just like you would with any HTTP
client::
def test_headers(api):
@api.route("/")
def view(req, resp):
resp.headers["X-Custom"] = "hello"
resp.cookies["session"] = "abc123"
r = api.requests.get("/")
assert r.headers["X-Custom"] == "hello"
assert "session" in r.cookies
Testing WebSockets
------------------
WebSocket tests use Starlette's ``TestClient`` directly, since WebSocket
connections require a different protocol. The ``websocket_connect`` context
manager gives you a connection you can send and receive on::
from starlette.testclient import TestClient
def test_websocket(api):
@api.route("/ws", websocket=True)
async def ws(ws):
await ws.accept()
name = await ws.receive_text()
await ws.send_text(f"hello, {name}!")
await ws.close()
client = TestClient(api)
with client.websocket_connect("/ws") as ws:
ws.send_text("world")
assert ws.receive_text() == "hello, world!"
Testing Error Handling
----------------------
By default, the test client raises exceptions from your route handlers,
which is usually what you want — it makes bugs obvious. But when you're
testing error handling specifically, you want to see the error response
instead. Disable exception propagation with ``raise_server_exceptions``::
from starlette.testclient import TestClient
def test_500(api):
@api.route("/fail")
def fail(req, resp):
raise ValueError("something broke")
client = TestClient(api, raise_server_exceptions=False)
r = client.get(api.url_for(fail))
assert r.status_code == 500
If you've registered a custom exception handler, you can test that too::
def test_custom_error(api):
@api.exception_handler(ValueError)
async def handle(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
@api.route("/fail")
def fail(req, resp):
raise ValueError("bad input")
client = TestClient(api, raise_server_exceptions=False)
r = client.get(api.url_for(fail))
assert r.status_code == 400
assert r.json() == {"error": "bad input"}
Testing Lifespan Events
-----------------------
If your app uses startup and shutdown events (for database connections,
caches, etc.), you need the test client to trigger them. Wrap the client
in a ``with`` block — startup runs on enter, shutdown runs on exit::
def test_with_lifespan(api):
started = {"value": False}
@api.on_event("startup")
async def on_startup():
started["value"] = True
@api.route("/")
def check(req, resp):
resp.media = {"started": started["value"]}
with api.requests as session:
r = session.get("http://;/")
assert r.json() == {"started": True}
Without the ``with`` block, lifespan events won't fire, which can lead to
confusing test failures if your routes depend on startup initialization.
Testing Before and After Hooks
------------------------------
Before-request and after-request hooks run automatically during tests,
just like in production. You can verify their effects on the response::
def test_hooks(api):
@api.route(before_request=True)
def add_version(req, resp):
resp.headers["X-Version"] = "3.2"
@api.after_request()
def add_timing(req, resp):
resp.headers["X-Served-By"] = "responder"
@api.route("/")
def view(req, resp):
resp.text = "ok"
r = api.requests.get("/")
assert r.headers["X-Version"] == "3.2"
assert r.headers["X-Served-By"] == "responder"
Tips
----
- **Keep tests fast.** The in-process test client is already fast — no
network overhead. Avoid ``time.sleep()`` in tests.
- **One API per test** when testing configuration. If you need a specific
``API()`` configuration (like ``cors=True``), create a new instance
in the test rather than sharing the fixture.
- Use ``api.url_for()`` instead of hard-coded paths. It's a small
thing, but it makes refactoring painless.
- **Test the contract, not the implementation.** Assert on status codes,
response bodies, and headers — not on internal state.
+523 -337
View File
@@ -1,41 +1,276 @@
Feature Tour
============
This section walks through Responder's features in depth. Each section
explains the concept, shows working code, and explains the design choices
behind it. If you're new to web development, this is a good place to learn
how modern web frameworks work under the hood.
Method Filtering
----------------
HTTP defines several *methods* (also called verbs) that describe what a
client wants to do with a resource. The most common are:
- ``GET`` — retrieve data
- ``POST`` — create something new
- ``PUT`` — replace something entirely
- ``PATCH`` — update part of something
- ``DELETE`` — remove something
By default, a Responder route matches all methods. This is fine for simple
endpoints, but REST APIs typically map different methods to different
operations. Use the ``methods`` parameter to restrict a route::
@api.route("/items", methods=["GET"])
def list_items(req, resp):
resp.media = {"items": []}
@api.route("/items", methods=["POST"], check_existing=False)
async def create_item(req, resp):
data = await req.media()
resp.media = {"created": data}
Note the ``check_existing=False`` — Responder normally prevents you from
registering two routes with the same path (to catch typos). When you
intentionally want multiple handlers for the same path with different
methods, you need to opt in.
Class-Based Views
-----------------
Class-based views (and setting some headers and stuff)::
Function-based views are great for simple endpoints, but sometimes you want
to group related HTTP methods together into a single resource. This is
where class-based views come in — a pattern popularized by
`Falcon <https://falconframework.org/>`_.
Responder dispatches to the appropriate method handler based on the HTTP
method::
@api.route("/{greeting}")
class GreetingResource:
def on_request(self, req, resp, *, greeting): # or on_get...
def on_get(self, req, resp, *, greeting):
resp.text = f"{greeting}, world!"
resp.headers.update({'X-Life': '42'})
resp.status_code = api.status_codes.HTTP_416
def on_post(self, req, resp, *, greeting):
resp.media = {"received": greeting}
def on_request(self, req, resp, *, greeting):
"""Called on EVERY request, before the method-specific handler."""
resp.headers["X-Greeting"] = greeting
The ``on_request`` method is called for all HTTP methods, much like
middleware scoped to a single route. Method-specific handlers (``on_get``,
``on_post``, ``on_put``, ``on_delete``, etc.) are called after.
No inheritance required — just define a class with the right method names.
This is simpler than Django's ``View`` classes and more Pythonic than
framework-specific base classes.
Background Tasks
----------------
Lifespan Events
---------------
Here, you can spawn off a background thread to run any function, out-of-request::
Real applications need to set up resources when they start (database
connection pools, ML models, caches) and tear them down when they stop.
This is called the application *lifespan*.
@api.route("/")
def hello(req, resp):
The modern approach is the *context manager* pattern, where startup and
shutdown are two halves of the same block::
@api.background.task
def sleep(s=10):
time.sleep(s)
print("slept!")
from contextlib import asynccontextmanager
sleep()
resp.content = "processing"
@asynccontextmanager
async def lifespan(app):
# Startup — runs before the first request
print("connecting to database...")
yield
# Shutdown — runs after the server stops
print("closing connections...")
api = responder.API(lifespan=lifespan)
Everything before ``yield`` runs at startup. Everything after runs at
shutdown. If startup fails, the server won't start. If shutdown raises,
it's logged but the server still exits.
The traditional event decorator style also works::
@api.on_event("startup")
async def startup():
print("starting up")
@api.on_event("shutdown")
async def shutdown():
print("shutting down")
The context manager is preferred for new code — it keeps related startup
and shutdown logic together and makes resource cleanup more explicit.
Serving Files
-------------
Web applications often need to serve files — downloads, reports, images.
Responder makes this simple with ``resp.file()``, which reads a file from
disk and sets the ``Content-Type`` header automatically using Python's
``mimetypes`` module::
@api.route("/download")
def download(req, resp):
resp.file("reports/annual.pdf")
You can override the content type if the automatic detection isn't right::
@api.route("/image")
def image(req, resp):
resp.file("photos/cat.jpg", content_type="image/jpeg")
For large files, use ``resp.stream_file()`` to avoid loading the entire
file into memory. This streams the file in chunks::
@api.route("/export")
def export(req, resp):
resp.stream_file("data/export.csv")
Custom Error Handling
---------------------
In production, you don't want your users to see raw Python tracebacks.
Responder lets you register custom handlers for specific exception types,
so you can return clean, structured error responses::
@api.exception_handler(ValueError)
async def handle_value_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
Now, any route that raises a ``ValueError`` will return a clean JSON
response with a 400 status code instead of a generic 500 error page.
This is a common pattern in API development — you define your own exception
classes for different error conditions, register handlers for each, and
your API always returns consistent, machine-readable error responses.
Before-Request Hooks
--------------------
Sometimes you need to run the same code before every request —
authentication checks, request logging, adding common headers, or setting
up per-request state. Before-request hooks let you do this without
duplicating code in every route::
@api.route(before_request=True)
def add_headers(req, resp):
resp.headers["X-API-Version"] = "3.2"
**Short-circuiting** is the really powerful part. If your hook sets
``resp.status_code``, the route handler is skipped entirely and the
response is sent immediately. This is the pattern for authentication::
@api.route(before_request=True)
def auth_check(req, resp):
if "Authorization" not in req.headers:
resp.status_code = 401
resp.media = {"error": "unauthorized"}
If the ``Authorization`` header is missing, the client gets a 401 response
and the actual route handler never runs. This is cleaner than adding
auth checks to every individual route.
After-Request Hooks
-------------------
The complement to before-request hooks. After-request hooks run after the
route handler completes but before the response is sent. They're useful
for logging, adding response headers, or any post-processing::
@api.after_request()
def log_response(req, resp):
print(f"{req.method} {req.full_url} -> {resp.status_code}")
@api.after_request()
async def add_timing(req, resp):
resp.headers["X-Served-By"] = "responder"
WebSocket Support
-----------------
HTTP is a request-response protocol — the client asks, the server answers.
But some applications need real-time, bidirectional communication: chat
apps, live dashboards, multiplayer games, collaborative editors.
`WebSockets <https://en.wikipedia.org/wiki/WebSocket>`_ solve this by
upgrading an HTTP connection into a persistent, full-duplex channel where
both sides can send messages at any time::
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
await ws.close()
You can send and receive in multiple formats:
- ``send_text`` / ``receive_text`` — plain text strings
- ``send_json`` / ``receive_json`` — JSON objects (auto-serialized)
- ``send_bytes`` / ``receive_bytes`` — raw binary data
WebSocket routes are marked with ``websocket=True`` in the route decorator.
They receive a ``ws`` object instead of ``req`` and ``resp``.
Server-Sent Events (SSE)
-------------------------
SSE is a simpler alternative to WebSockets for *one-way* real-time
communication — the server pushes events to the client, but the client
can't send messages back. This is perfect for live feeds, progress bars,
notification streams, and AI response streaming.
Unlike WebSockets, SSE works over plain HTTP, is automatically reconnected
by the browser, and doesn't require any special client-side libraries::
@api.route("/events")
async def events(req, resp):
@resp.sse
async def stream():
for i in range(10):
yield {"data": f"message {i}"}
On the client side, you consume SSE events with JavaScript's built-in
``EventSource`` API::
const source = new EventSource("/events");
source.onmessage = (event) => {
console.log(event.data);
};
Each yielded value can be a string (treated as data) or a dict with the
standard SSE fields::
yield {"event": "update", "data": "hello", "id": "1", "retry": "5000"}
yield "simple string message"
GraphQL
-------
Serve a GraphQL API::
`GraphQL <https://graphql.org/>`_ is a query language for APIs that lets
clients request exactly the data they need — no more, no less. Instead of
multiple REST endpoints, you define a schema and let clients query it.
Responder includes built-in GraphQL support via
`Graphene <https://graphene-python.org/>`_. Set up a full GraphQL endpoint
with a single method call::
import graphene
@@ -45,383 +280,334 @@ Serve a GraphQL API::
def resolve_hello(self, info, name):
return f"Hello {name}"
schema = graphene.Schema(query=Query)
view = responder.ext.GraphQLView(api=api, schema=schema)
api.graphql("/graphql", schema=graphene.Schema(query=Query))
api.add_route("/graph", view)
Visiting ``/graphql`` in a browser renders the
`GraphiQL <https://github.com/graphql/graphiql>`_ interactive IDE, where
you can explore your schema, write queries, and see results in real-time.
Programmatic clients can POST JSON queries to the same endpoint.
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
You can make use of Responder's Request and Response objects in your GraphQL resolvers through ``info.context['request']`` and ``info.context['response']``.
You can access the Responder request and response objects in your resolvers
through ``info.context["request"]`` and ``info.context["response"]``.
OpenAPI Schema Support
----------------------
OpenAPI Documentation
---------------------
Responder comes with built-in support for OpenAPI / marshmallow
`OpenAPI <https://www.openapis.org/>`_ (formerly Swagger) is the industry
standard for describing REST APIs. An OpenAPI specification lets you
auto-generate interactive documentation, client libraries, and validation
logic.
New in Responder `1.4.0`::
import responder
from responder.ext.schema import Schema as OpenAPISchema
from marshmallow import Schema, fields
contact = {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "support@example.com",
}
license = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
api = responder.API()
schema = OpenAPISchema(
app=api,
title="Web Service",
version="1.0",
openapi="3.0.2",
description="A simple pet store",
terms_of_service="http://example.com/terms/",
contact=contact,
license=license,
)
@schema.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
@api.route("/")
def route(req, resp):
"""A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
"""
resp.media = PetSchema().dump({"name": "little orange"})
Old way *It's recommended to use the code above* ::
import responder
from marshmallow import Schema, fields
contact = {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "support@example.com",
}
license = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
Responder generates OpenAPI specs from your code::
api = responder.API(
title="Web Service",
title="Pet Store",
version="1.0",
openapi="3.0.2",
description="A simple pet store",
terms_of_service="http://example.com/terms/",
contact=contact,
license=license,
docs_route="/docs",
)
This gives you:
- An OpenAPI schema at ``/schema.yml``
- Interactive Swagger UI documentation at ``/docs``
There are three ways to document your endpoints.
**Pydantic models** — the recommended approach. Use ``request_model`` and
``response_model`` to annotate your routes, and Responder generates the
schema automatically. When ``request_model`` is set, request bodies are
also validated automatically — invalid inputs get a ``422`` response with
detailed error messages::
from pydantic import BaseModel
class PetIn(BaseModel):
name: str
age: int = 0
class PetOut(BaseModel):
id: int
name: str
age: int
@api.route("/pets", methods=["POST"],
request_model=PetIn, response_model=PetOut)
async def create_pet(req, resp):
data = await req.media()
resp.media = {"id": 1, **data}
When ``response_model`` is set, the response is serialized through the
model — extra fields are stripped and types are enforced.
**YAML docstrings** — for full control, embed OpenAPI YAML in the
docstring::
@api.route("/pets")
def list_pets(req, resp):
"""A list of pets.
---
get:
description: Get all pets
responses:
200:
description: A list of pets
"""
resp.media = [{"name": "Fido"}]
**Marshmallow schemas** — if you're already using marshmallow::
from marshmallow import Schema, fields
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
@api.route("/")
def route(req, resp):
"""A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
"""
resp.media = PetSchema().dump({"name": "little orange"})
::
>>> r = api.session().get("http://;/schema.yml")
>>> print(r.text)
components:
parameters: {}
responses: {}
schemas:
Pet:
properties:
name: {type: string}
type: object
securitySchemes: {}
info:
contact: {email: support@example.com, name: API Support, url: 'http://www.example.com/support'}
description: This is a sample server for a pet store.
license: {name: Apache 2.0, url: 'https://www.apache.org/licenses/LICENSE-2.0.html'}
termsOfService: http://example.com/terms/
title: Web Service
version: 1.0
openapi: 3.0.2
paths:
/:
get:
description: Get a random pet
responses:
200: {description: A pet to be returned, schema: $ref: "#/components/schemas/Pet"}
tags: []
All three approaches can be mixed in the same API. You can choose from
multiple documentation themes: ``swagger_ui`` (default), ``redoc``,
``rapidoc``, or ``elements``.
Interactive Documentation
-------------------------
Route Groups
------------
Responder can automatically supply API Documentation for you. Using the example above
As your application grows, you'll want to organize routes logically.
Route groups let you share a URL prefix across related endpoints — a
common pattern for API versioning::
The new and recommended way::
v1 = api.group("/v1")
...
from responder.ext.schema import Schema
...
api = responder.API()
@v1.route("/users")
def list_users(req, resp):
resp.media = []
schema = Schema(
app=api,
title="Web Service",
version="1.0",
openapi="3.0.2",
...
docs_route='/docs',
...
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license,
)
@v1.route("/users/{user_id:int}")
def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
The old way ::
v2 = api.group("/v2")
api = responder.API(
title="Web Service",
version="1.0",
openapi="3.0.2",
docs_route='/docs',
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license,
)
@v2.route("/users")
def list_users_v2(req, resp):
resp.media = {"users": [], "total": 0}
This will make ``/docs`` render interactive documentation for your API.
This keeps your code organized without affecting the routing logic.
Mount a WSGI / ASGI Apps (e.g. Flask, Starlette,...)
----------------------------------------------------
Responder gives you the ability to mount another ASGI / WSGI app at a subroute::
Mounting Other Apps
-------------------
Responder can mount any WSGI or ASGI application at a subroute. This is
incredibly useful for gradual migrations — you can run Flask and Responder
side by side, moving routes over one at a time::
import responder
from flask import Flask
api = responder.API()
flask = Flask(__name__)
flask_app = Flask(__name__)
@flask.route('/')
@flask_app.route("/")
def hello():
return 'hello'
return "Hello from Flask!"
api.mount('/flask', flask)
api.mount("/flask", flask_app)
That's it!
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.
Single-Page Web Apps
--------------------
You can also mount `marimo <https://marimo.io/>`_ notebooks as
interactive dashboards within your API::
If you have a single-page webapp, you can tell Responder to serve up your ``static/index.html`` at a route, like so::
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
-------
`Cookies <https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies>`_ are
small pieces of data that the server asks the browser to store and send
back with every subsequent request. They're the foundation of sessions,
authentication tokens, and user preferences on the web.
Reading and writing cookies is straightforward::
# Read cookies from the request
session_id = req.cookies.get("session_id")
# Set a cookie on the response
resp.cookies["hello"] = "world"
For production use, you'll want to set security directives. The
``httponly`` flag prevents JavaScript from reading the cookie (defending
against XSS attacks), and ``secure`` ensures it's only sent over HTTPS::
resp.set_cookie(
"token",
value="abc123",
max_age=3600, # expires in 1 hour
secure=True, # HTTPS only
httponly=True, # no JavaScript access
path="/",
)
Cookie-Based Sessions
---------------------
Sessions let you store per-user data across multiple requests. Responder's
built-in sessions are cookie-based — the session data is serialized, signed
with your secret key, and stored in a cookie. The signature prevents
tampering: if someone modifies the cookie, the signature won't match and
the data will be rejected::
@api.route("/login")
def login(req, resp):
resp.session["username"] = "alice"
@api.route("/profile")
def profile(req, resp):
resp.media = {"user": req.session.get("username")}
.. warning::
Always set a secret key in production. The default key is not secret::
api = responder.API(secret_key="your-secret-key-here")
Static Files
------------
Most web applications serve static assets — CSS stylesheets, JavaScript
files, images, fonts. Responder serves these from the ``static/`` directory
by default::
api = responder.API(static_dir="static", static_route="/static")
Place your assets in the ``static/`` directory and they'll be served
automatically at ``/static/style.css``, ``/static/app.js``, etc.
For single-page applications (React, Vue, Angular), you can serve
``index.html`` as the default response for all unmatched routes::
api.add_route("/", static=True)
This will make ``index.html`` the default response to all undefined routes.
Reading / Writing Cookies
-------------------------
Responder makes it very easy to interact with cookies from a Request, or add some to a Response::
>>> resp.cookies["hello"] = "world"
>>> req.cookies
{"hello": "world"}
To set cookies directives, you should use `resp.set_cookie`::
>>> resp.set_cookie("hello", value="world", max_age=60)
Supported directives:
* ``key`` - **Required**
* ``value`` - [OPTIONAL] - Defaults to ``""``.
* ``expires`` - Defaults to ``None``.
* ``max_age`` - Defaults to ``None``.
* ``domain`` - Defaults to ``None``.
* ``path`` - Defaults to ``"/"``.
* ``secure`` - Defaults to ``False``.
* ``httponly`` - Defaults to ``True``.
For more information see `directives <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie>`_
Using Cookie-Based Sessions
---------------------------
Responder has built-in support for cookie-based sessions. To enable cookie-based sessions, simply add something to the ``resp.session`` dictionary::
>>> resp.session['username'] = 'kennethreitz'
A cookie called ``Responder-Session`` will be set, which contains all the data in ``resp.session``. It is signed, for verification purposes.
You can easily read a Request's session data, that can be trusted to have originated from the API::
>>> req.session
{'username': 'kennethreitz'}
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``::
api = responder.API(secret_key=os.environ['SECRET_KEY'])
Using ``before_request``
------------------------
If you'd like a view to be executed before every request, simply do the following::
@api.route(before_request=True)
def prepare_response(req, resp):
resp.headers["X-Pizza"] = "42"
Now all requests to your HTTP Service will include an ``X-Pizza`` header.
For ``websockets``::
@api.route(before_request=True, websocket=True)
def prepare_response(ws):
await ws.accept()
WebSocket Support
-----------------
Responder supports WebSockets::
@api.route('/ws', websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
await ws.close()
Accepting the connection::
await websocket.accept()
Sending and receiving data::
await websocket.send_{format}(data)
await websocket.receive_{format}(data)
Supported formats: ``text``, ``json``, ``bytes``.
Closing the connection::
await websocket.close()
Using Requests Test Client
--------------------------
Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
Here's an example of a test (written with pytest)::
import myapi
@pytest.fixture
def api():
return myapi.api
def test_response(api):
hello = "hello, world!"
@api.route('/some-url')
def some_view(req, resp):
resp.text = hello
r = api.requests.get(url=api.url_for(some_view))
assert r.text == hello
HSTS (Redirect to HTTPS)
------------------------
Want HSTS (to redirect all traffic to HTTPS)?
::
api = responder.API(enable_hsts=True)
Boom.
CORS
----
Want `CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/>`_ ?
`CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_ (Cross-
Origin Resource Sharing) is a security mechanism that controls which
websites can make requests to your API. Browsers enforce this — if your
API is at ``api.example.com`` and your frontend is at ``app.example.com``,
the browser will block requests unless your API explicitly allows it.
::
Enable CORS and configure which origins are allowed::
api = responder.API(cors=True)
api = responder.API(cors=True, cors_params={
"allow_origins": ["https://app.example.com"],
"allow_methods": ["GET", "POST"],
"allow_headers": ["*"],
"allow_credentials": True,
"max_age": 600,
})
The default policy is restrictive — you must explicitly allow each origin.
Using ``["*"]`` for allow_origins permits any website to call your API,
which is fine for public APIs but not for private ones.
The default parameters used by **Responder** are restrictive by default, so you'll need to explicitly enable particular origins, methods, or headers, in order for browsers to be permitted to use them in a Cross-Domain context.
HSTS
----
In order to set custom parameters, you need to set the ``cors_params`` argument of ``api``, a dictionary containing the following entries:
`HSTS <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security>`_
(HTTP Strict Transport Security) tells browsers to always use HTTPS when
communicating with your server. Once a browser sees the HSTS header, it
will refuse to connect over plain HTTP, even if the user types ``http://``
in the address bar::
api = responder.API(enable_hsts=True)
* ``allow_origins`` - A list of origins that should be permitted to make cross-origin requests. eg. ``['https://example.org', 'https://www.example.org']``. You can use ``['*']`` to allow any origin.
* ``allow_origin_regex`` - A regex string to match against origins that should be permitted to make cross-origin requests. eg. ``'https://.*\.example\.org'``.
* ``allow_methods`` - A list of HTTP methods that should be allowed for cross-origin requests. Defaults to `['GET']`. You can use ``['*']`` to allow all standard methods.
* ``allow_headers`` - A list of HTTP request headers that should be supported for cross-origin requests. Defaults to ``[]``. You can use ``['*']`` to allow all headers. The ``Accept``, ``Accept-Language``, ``Content-Language`` and ``Content-Type`` headers are always allowed for CORS requests.
* ``allow_credentials`` - Indicate that cookies should be supported for cross-origin requests. Defaults to ``False``.
* ``expose_headers`` - Indicate any response headers that should be made accessible to the browser. Defaults to ``[]``.
* ``max_age`` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to ``60``.
Trusted Hosts
-------------
Make sure that all the incoming requests headers have a valid ``host``, that matches one of the provided patterns in the ``allowed_hosts`` attribute, in order to prevent HTTP Host Header attacks.
The ``Host`` header in an HTTP request tells the server which domain name
the client used. Attackers can forge this header to trick your application
into generating URLs to malicious domains (a class of attack called *Host
header injection*).
A 400 response will be raised, if a request does not match any of the provided patterns in the ``allowed_hosts`` attribute.
Restrict which hostnames your application accepts::
::
api = responder.API(allowed_hosts=["example.com", "*.example.com"])
api = responder.API(allowed_hosts=['example.com', 'tenant.example.com'])
Requests with unrecognized hosts get a ``400 Bad Request``. Wildcard
patterns are supported. By default, all hostnames are allowed.
* ``allowed_hosts`` - A list of allowed hostnames.
Note:
Request ID
----------
* By default, all hostnames are allowed.
* Wildcard domains such as ``*.example.com`` are supported.
* To allow any hostname use ``allowed_hosts=["*"]``.
In distributed systems, tracing a single request across multiple services
is essential for debugging. Request IDs are unique identifiers attached to
each request — if something goes wrong, you can search your logs for that
ID and find every related event.
Responder can auto-generate request IDs. If the client sends an
``X-Request-ID`` header (common in microservice architectures), it's
forwarded. Otherwise, a new UUID is generated::
api = responder.API(request_id=True)
The ID appears in the ``X-Request-ID`` response header.
Rate Limiting
-------------
Rate limiting prevents individual clients from overwhelming your API with
too many requests. It's essential for public APIs, and good practice even
for internal ones.
Responder includes a built-in token bucket rate limiter::
from responder.ext.ratelimit import RateLimiter
limiter = RateLimiter(requests=100, period=60) # 100 req/min
limiter.install(api)
When the limit is exceeded, clients receive a ``429 Too Many Requests``
response with a ``Retry-After`` header. Every response includes
``X-RateLimit-Limit`` and ``X-RateLimit-Remaining`` headers so clients
can pace themselves.
The rate limiter is per-client, keyed by IP address.
MessagePack
-----------
`MessagePack <https://msgpack.org/>`_ is a binary serialization format
that's more compact and faster to parse than JSON. It's useful for
high-throughput APIs, IoT devices, and anywhere bandwidth matters.
Responder supports MessagePack alongside JSON and YAML::
# Decode a MessagePack request body
data = await req.media("msgpack")
Content negotiation works too — clients can send
``Accept: application/x-msgpack`` to receive MessagePack responses
instead of JSON.
+191
View File
@@ -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 <https://jwt.io/>`_ (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 <token>`` 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 = "<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.
+192
View File
@@ -0,0 +1,192 @@
Migrating from Flask
====================
If you're coming from Flask, you'll find Responder familiar but different
in a few key ways. This guide maps Flask concepts to their Responder
equivalents and shows you how to translate common patterns.
The Big Differences
-------------------
**No return values.** In Flask, you return a response. In Responder, you
mutate it. This is the single biggest difference:
Flask::
@app.route("/")
def hello():
return "hello, world!"
Responder::
@api.route("/")
def hello(req, resp):
resp.text = "hello, world!"
**Explicit request and response.** Flask uses a global ``request`` object
(via thread-local magic). Responder passes ``req`` and ``resp`` explicitly.
No magic, no import needed — they're right there in the function signature.
**ASGI, not WSGI.** Flask runs on WSGI, which is synchronous. Responder
runs on ASGI, which supports async natively. You can still write sync
views — Responder runs them in a thread pool automatically.
Quick Reference
---------------
.. list-table::
:header-rows: 1
:widths: 40 60
* - Flask
- Responder
* - ``Flask(__name__)``
- ``responder.API()``
* - ``return "text"``
- ``resp.text = "text"``
* - ``return jsonify(data)``
- ``resp.media = data``
* - ``return render_template("t.html", x=1)``
- ``resp.html = api.template("t.html", x=1)``
* - ``request.args["q"]``
- ``req.params["q"]``
* - ``request.json``
- ``await req.media()``
* - ``request.form``
- ``await req.media("form")``
* - ``request.headers["X"]``
- ``req.headers["X"]``
* - ``request.method``
- ``req.method``
* - ``request.cookies["x"]``
- ``req.cookies["x"]``
* - ``session["x"] = 1``
- ``resp.session["x"] = 1``
* - ``abort(404)``
- ``resp.status_code = 404``
* - ``redirect("/new")``
- ``api.redirect(resp, location="/new")``
* - ``@app.before_request``
- ``@api.route(before_request=True)``
* - ``@app.errorhandler(404)``
- ``@api.exception_handler(ValueError)``
* - ``app.run(debug=True)``
- ``api.run(debug=True)``
Route Parameters
----------------
Flask uses ``<angle_brackets>``. Responder uses ``{curly_braces}``
with the same type convertor idea:
Flask::
@app.route("/users/<int:user_id>")
def get_user(user_id):
return jsonify({"id": user_id})
Responder::
@api.route("/users/{user_id:int}")
def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
Note the ``*`` — route parameters are keyword-only arguments in
Responder. This makes the interface explicit about which arguments
come from the URL.
JSON APIs
---------
Flask::
@app.route("/api/items", methods=["POST"])
def create_item():
data = request.json
# ... create item
return jsonify(item), 201
Responder::
@api.route("/api/items", methods=["POST"])
async def create_item(req, resp):
data = await req.media()
# ... create item
resp.media = item
resp.status_code = 201
The ``await`` is needed because reading the request body is an async
I/O operation. This is more explicit than Flask's approach, and it
means the event loop isn't blocked while waiting for the body to arrive.
Templates
---------
Both use Jinja2. The syntax is nearly identical:
Flask::
@app.route("/hello/<name>")
def hello(name):
return render_template("hello.html", name=name)
Responder::
@api.route("/hello/{name}")
def hello(req, resp, *, name):
resp.html = api.template("hello.html", name=name)
Blueprints → Route Groups
--------------------------
Flask uses Blueprints to organize routes. Responder has route groups:
Flask::
bp = Blueprint("api", __name__, url_prefix="/api")
@bp.route("/users")
def list_users():
return jsonify([])
app.register_blueprint(bp)
Responder::
api_v1 = api.group("/api")
@api_v1.route("/users")
def list_users(req, resp):
resp.media = []
Gradual Migration
-----------------
You don't have to migrate all at once. Responder can mount your existing
Flask app at a subroute, so you can move endpoints over one at a time::
from flask import Flask
flask_app = Flask(__name__)
# Your existing Flask routes stay here
@flask_app.route("/legacy")
def legacy():
return "old endpoint"
# Mount Flask under /old, new routes go on Responder
api.mount("/old", flask_app)
@api.route("/new")
def new_endpoint(req, resp):
resp.media = {"modern": True}
Requests to ``/old/legacy`` go to Flask. Everything else goes to
Responder. When you've moved everything over, remove the mount.
+129
View File
@@ -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.
+219
View File
@@ -0,0 +1,219 @@
Building a REST API
===================
This tutorial walks you through building a complete REST API from scratch.
By the end, you'll have a working API with CRUD operations, request
validation, error handling, and interactive documentation.
We'll build a simple book catalog — a service that lets you create, read,
update, and delete books.
Project Setup
-------------
Create a new file called ``app.py``::
import responder
api = responder.API(
title="Book Catalog",
version="1.0",
openapi="3.0.2",
docs_route="/docs",
)
We're enabling OpenAPI documentation from the start. Visit ``/docs`` at
any point to see interactive Swagger UI for your API.
Define Your Models
------------------
We'll use `Pydantic <https://docs.pydantic.dev/>`_ to define our data
models. Pydantic models serve double duty — they validate incoming data
*and* generate OpenAPI schemas automatically::
from pydantic import BaseModel
class BookIn(BaseModel):
"""What the client sends when creating a book."""
title: str
author: str
year: int
isbn: str | None = None
class Book(BaseModel):
"""What the API returns."""
id: int
title: str
author: str
year: int
isbn: str | None = None
``BookIn`` is the *input* model — it doesn't have an ``id`` because the
server assigns that. ``Book`` is the *output* model — it includes
everything. This input/output separation is a common REST API pattern.
In-Memory Storage
-----------------
For this tutorial, we'll store books in a simple dict. In a real
application, you'd use a database (see :doc:`tutorial-sqlalchemy`)::
books_db: dict[int, dict] = {}
next_id = 1
List All Books
--------------
The first endpoint — list all books. This is a ``GET`` request to
``/books``::
@api.route("/books", methods=["GET"], response_model=list)
def list_books(req, resp):
resp.media = list(books_db.values())
In REST API design, ``GET`` requests should never modify data. They're
*safe* and *idempotent* — calling them multiple times has the same effect
as calling them once.
Create a Book
-------------
To create a book, the client sends a ``POST`` request with a JSON body.
We use ``request_model=BookIn`` to validate the input automatically — if
the client sends bad data, they get a ``422`` response with error details::
@api.route("/books", methods=["POST"], check_existing=False,
request_model=BookIn, response_model=Book)
async def create_book(req, resp):
global next_id
data = await req.media()
book = {"id": next_id, **data}
books_db[next_id] = book
next_id += 1
resp.media = book
resp.status_code = 201
Note ``resp.status_code = 201`` — the HTTP ``201 Created`` status code
tells the client that a new resource was successfully created. This is
more informative than a generic ``200 OK``.
Get a Single Book
-----------------
Retrieve a specific book by its ID. The ``{book_id:int}`` route parameter
ensures only integer IDs match — requests like ``/books/abc`` will 404::
@api.route("/books/{book_id:int}", methods=["GET"], response_model=Book)
def get_book(req, resp, *, book_id):
if book_id not in books_db:
resp.status_code = 404
resp.media = {"error": f"Book {book_id} not found"}
return
resp.media = books_db[book_id]
Update a Book
-------------
``PUT`` replaces a resource entirely. The client must send all fields::
@api.route("/books/{book_id:int}", methods=["PUT"], check_existing=False,
request_model=BookIn, response_model=Book)
async def update_book(req, resp, *, book_id):
if book_id not in books_db:
resp.status_code = 404
resp.media = {"error": f"Book {book_id} not found"}
return
data = await req.media()
book = {"id": book_id, **data}
books_db[book_id] = book
resp.media = book
Delete a Book
-------------
``DELETE`` removes a resource. The convention is to return ``204 No Content``
with an empty body on success::
@api.route("/books/{book_id:int}", methods=["DELETE"], check_existing=False)
def delete_book(req, resp, *, book_id):
if book_id not in books_db:
resp.status_code = 404
resp.media = {"error": f"Book {book_id} not found"}
return
del books_db[book_id]
resp.status_code = 204
Error Handling
--------------
Let's add a custom error handler so any ``ValueError`` in our code returns
a clean JSON response instead of a 500 error::
@api.exception_handler(ValueError)
async def handle_value_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
Run It
------
Add the standard entry point at the bottom of your file::
if __name__ == "__main__":
api.run()
Start the server::
$ python app.py
Visit ``http://localhost:5042/docs`` to see your interactive API
documentation. You can test every endpoint directly from the browser.
Try It Out
----------
Using ``curl``::
# Create a book
$ curl -X POST http://localhost:5042/books \
-H "Content-Type: application/json" \
-d '{"title": "Dune", "author": "Frank Herbert", "year": 1965}'
# List all books
$ curl http://localhost:5042/books
# Get a specific book
$ curl http://localhost:5042/books/1
# Update a book
$ curl -X PUT http://localhost:5042/books/1 \
-H "Content-Type: application/json" \
-d '{"title": "Dune", "author": "Frank Herbert", "year": 1965, "isbn": "978-0441172719"}'
# Delete a book
$ curl -X DELETE http://localhost:5042/books/1
What's Next
-----------
This tutorial used in-memory storage. For a real application, you'll want
a database. See :doc:`tutorial-sqlalchemy` for how to integrate SQLAlchemy
with Responder using the lifespan pattern.
+254
View File
@@ -0,0 +1,254 @@
Using SQLAlchemy
================
Most real web applications need a database. This guide shows how to
integrate `SQLAlchemy <https://www.sqlalchemy.org/>`_ with Responder,
using async support and the lifespan pattern for connection management.
SQLAlchemy is the most popular Python database toolkit. It gives you an
ORM (Object-Relational Mapper) for working with databases using Python
classes instead of raw SQL, plus a powerful query builder for when you
need fine-grained control.
Installation
------------
Install SQLAlchemy with async support and an async database driver.
We'll use SQLite for simplicity, but the pattern works with PostgreSQL,
MySQL, and any other database SQLAlchemy supports::
$ uv pip install 'sqlalchemy[asyncio]' aiosqlite
Define Your Models
------------------
SQLAlchemy models map Python classes to database tables. Each attribute
becomes a column::
# models.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
class Book(Base):
__tablename__ = "books"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String, nullable=False)
author = Column(String, nullable=False)
year = Column(Integer, nullable=False)
isbn = Column(String, nullable=True)
``DeclarativeBase`` is SQLAlchemy's modern base class (SQLAlchemy 2.0+).
Each model class corresponds to a table, and each ``Column`` corresponds
to a column in that table.
Database Setup
--------------
Create the async engine and session factory. The *engine* manages
the connection pool. The *session* is your unit of work — you use it to
query and modify data within a transaction::
# database.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
DATABASE_URL = "sqlite+aiosqlite:///./books.db"
engine = create_async_engine(DATABASE_URL, echo=True)
async_session = async_sessionmaker(engine, expire_on_commit=False)
The ``echo=True`` flag prints all SQL queries to the console — very
helpful during development, but you'll want to disable it in production.
The ``expire_on_commit=False`` flag keeps model attributes accessible
after a commit, which is convenient for returning created objects in
API responses.
Lifespan for Startup and Shutdown
----------------------------------
Use Responder's lifespan context manager to create the database tables
on startup and dispose of connections on shutdown::
# app.py
from contextlib import asynccontextmanager
import responder
from database import engine
from models import Base
@asynccontextmanager
async def lifespan(app):
# Startup: create tables
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
# Shutdown: close all connections
await engine.dispose()
api = responder.API(lifespan=lifespan)
This is the proper way to manage database connections in an async
application. The lifespan context manager ensures that:
1. Tables are created before the first request
2. The connection pool is properly closed when the server shuts down
3. If table creation fails, the server won't start
CRUD Endpoints
--------------
Now let's build the API endpoints. Each one opens a database session,
does its work, and commits or rolls back::
from pydantic import BaseModel
from sqlalchemy import select
from database import async_session
from models import Book
# Pydantic models for request/response validation
class BookIn(BaseModel):
title: str
author: str
year: int
isbn: str | None = None
class BookOut(BaseModel):
id: int
title: str
author: str
year: int
isbn: str | None = None
class Config:
from_attributes = True
The ``from_attributes = True`` config tells Pydantic to read data from
SQLAlchemy model attributes (not just dicts). This lets you pass a
SQLAlchemy ``Book`` object directly to ``BookOut``.
**List all books**::
@api.route("/books", methods=["GET"])
async def list_books(req, resp):
async with async_session() as session:
result = await session.execute(select(Book))
books = result.scalars().all()
resp.media = [BookOut.model_validate(b).model_dump() for b in books]
**Create a book**::
@api.route("/books", methods=["POST"], check_existing=False,
request_model=BookIn, response_model=BookOut)
async def create_book(req, resp):
data = await req.media()
async with async_session() as session:
book = Book(**data)
session.add(book)
await session.commit()
await session.refresh(book)
resp.media = BookOut.model_validate(book).model_dump()
resp.status_code = 201
**Get a single book**::
@api.route("/books/{book_id:int}", methods=["GET"])
async def get_book(req, resp, *, book_id):
async with async_session() as session:
book = await session.get(Book, book_id)
if book is None:
resp.status_code = 404
resp.media = {"error": "Book not found"}
return
resp.media = BookOut.model_validate(book).model_dump()
**Update a book**::
@api.route("/books/{book_id:int}", methods=["PUT"], check_existing=False,
request_model=BookIn)
async def update_book(req, resp, *, book_id):
data = await req.media()
async with async_session() as session:
book = await session.get(Book, book_id)
if book is None:
resp.status_code = 404
resp.media = {"error": "Book not found"}
return
for key, value in data.items():
setattr(book, key, value)
await session.commit()
await session.refresh(book)
resp.media = BookOut.model_validate(book).model_dump()
**Delete a book**::
@api.route("/books/{book_id:int}", methods=["DELETE"], check_existing=False)
async def delete_book(req, resp, *, book_id):
async with async_session() as session:
book = await session.get(Book, book_id)
if book is None:
resp.status_code = 404
resp.media = {"error": "Book not found"}
return
await session.delete(book)
await session.commit()
resp.status_code = 204
Run It
------
::
if __name__ == "__main__":
api.run()
Start the server and you'll see SQLAlchemy's SQL echo in the console.
The SQLite database file ``books.db`` is created automatically on first
startup.
Using PostgreSQL
----------------
To switch to PostgreSQL, just change the connection URL and driver::
$ uv pip install asyncpg
::
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/mydb"
Everything else stays the same. SQLAlchemy abstracts the database
differences so your application code doesn't need to change.
Tips
----
- Use ``async with async_session() as session`` for every request.
Don't share sessions across requests — each request should get its
own session and transaction.
- For complex queries, use SQLAlchemy's ``select()`` with ``.where()``,
``.order_by()``, ``.limit()``, and ``.offset()`` — it composes
naturally.
- In production, use connection pooling (SQLAlchemy does this by
default) and set pool size limits appropriate for your database.
- Consider `Alembic <https://alembic.sqlalchemy.org/>`_ for database
migrations — it tracks schema changes over time so you can evolve
your database without losing data.
+171
View File
@@ -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
<!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.
+7
View File
@@ -1,8 +1,15 @@
# Example HTTP service definition, using Responder.
# https://pypi.org/project/responder/
import responder
api = responder.API()
@api.route("/")
async def index(req, resp):
resp.text = "hello, world!"
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
+26
View File
@@ -0,0 +1,26 @@
# Example showing the lifespan context manager pattern.
# https://pypi.org/project/responder/
from contextlib import asynccontextmanager
import responder
@asynccontextmanager
async def lifespan(app):
# Startup: initialize resources
print("Starting up...")
yield
# Shutdown: clean up resources
print("Shutting down...")
api = responder.API(lifespan=lifespan)
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == "__main__":
api.run()
+31
View File
@@ -0,0 +1,31 @@
"""Mount marimo notebooks inside a Responder API.
Requirements:
pip install responder marimo
Usage:
python examples/marimo_mount.py
Then visit:
http://127.0.0.1:5042/hello → Responder JSON endpoint
http://127.0.0.1:5042/notebooks/ → Interactive marimo notebook
"""
import marimo
import responder
api = responder.API()
@api.route("/hello")
def hello(req, resp):
resp.media = {"message": "Hello from Responder!"}
# Mount marimo notebooks at /notebooks
server = marimo.create_asgi_app().with_app(path="", root="notebooks/hello.py")
api.mount("/notebooks", server.build())
if __name__ == "__main__":
api.run()
+76
View File
@@ -0,0 +1,76 @@
# Complete REST API example with Pydantic validation.
# https://responder.kennethreitz.org/tutorial-rest.html
from pydantic import BaseModel
import responder
class BookIn(BaseModel):
title: str
author: str
year: int
isbn: str | None = None
class BookOut(BaseModel):
id: int
title: str
author: str
year: int
isbn: str | None = None
api = responder.API(
title="Book Catalog",
version="1.0",
openapi="3.0.2",
docs_route="/docs",
)
books_db: dict[int, dict] = {}
next_id = 1
@api.route("/books", methods=["GET"])
def list_books(req, resp):
resp.media = list(books_db.values())
@api.route(
"/books",
methods=["POST"],
check_existing=False,
request_model=BookIn,
response_model=BookOut,
)
async def create_book(req, resp):
global next_id
data = await req.media()
book = {"id": next_id, **data}
books_db[next_id] = book
next_id += 1
resp.media = book
resp.status_code = 201
@api.route("/books/{book_id:int}", methods=["GET"])
def get_book(req, resp, *, book_id):
if book_id not in books_db:
resp.status_code = 404
resp.media = {"error": f"Book {book_id} not found"}
return
resp.media = books_db[book_id]
@api.route("/books/{book_id:int}", methods=["DELETE"], check_existing=False)
def delete_book(req, resp, *, book_id):
if book_id not in books_db:
resp.status_code = 404
resp.media = {"error": f"Book {book_id} not found"}
return
del books_db[book_id]
resp.status_code = 204
if __name__ == "__main__":
api.run()
+42
View File
@@ -0,0 +1,42 @@
# Server-Sent Events streaming example.
# https://responder.kennethreitz.org/tour.html#server-sent-events-sse
import asyncio
import responder
api = responder.API()
@api.route("/")
def index(req, resp):
resp.html = """
<!DOCTYPE html>
<html>
<body>
<h1>SSE Stream</h1>
<div id="events"></div>
<script>
const source = new EventSource("/stream");
const events = document.getElementById("events");
source.onmessage = (e) => {
const p = document.createElement("p");
p.textContent = e.data;
events.appendChild(p);
};
</script>
</body>
</html>
"""
@api.route("/stream")
async def stream(req, resp):
@resp.sse
async def events():
for i in range(20):
yield {"data": f"Event #{i}"}
await asyncio.sleep(0.5)
if __name__ == "__main__":
api.run()
+25
View File
@@ -0,0 +1,25 @@
# Example HTTP service definition, using Responder.
# https://pypi.org/project/responder/
import responder
api = responder.API()
@api.route("/")
async def index(req, resp):
resp.text = "Welcome"
@api.route("/user")
async def user_create(req, resp):
data = await req.media()
resp.text = f"Hello, {data['username']}"
@api.route("/user/{identifier}")
async def user_get(req, resp, *, identifier):
resp.text = f"Hello, user {identifier}"
if __name__ == "__main__":
api.run()
+59
View File
@@ -0,0 +1,59 @@
# WebSocket chat room example.
# https://responder.kennethreitz.org/tutorial-websockets.html
from starlette.websockets import WebSocketDisconnect
import responder
api = responder.API()
connected = set()
@api.route("/")
def index(req, resp):
resp.html = """
<!DOCTYPE html>
<html>
<body>
<h1>Chat Room</h1>
<div id="messages" style="height:300px;overflow-y:scroll;border:1px solid #ccc;padding:10px;"></div>
<input id="input" placeholder="Type a message..." style="width:300px;" />
<script>
const ws = new WebSocket(`ws://${location.host}/chat`);
const messages = document.getElementById("messages");
const input = document.getElementById("input");
ws.onmessage = (e) => {
const p = document.createElement("p");
p.textContent = e.data;
messages.appendChild(p);
messages.scrollTop = messages.scrollHeight;
};
input.addEventListener("keypress", (e) => {
if (e.key === "Enter" && input.value) {
ws.send(input.value);
input.value = "";
}
});
</script>
</body>
</html>
""" # noqa: E501
@api.route("/chat", websocket=True)
async def chat(ws):
await ws.accept()
connected.add(ws)
try:
while True:
message = await ws.receive_text()
for client in connected:
await client.send_text(message)
except WebSocketDisconnect:
pass
finally:
connected.discard(ws)
if __name__ == "__main__":
api.run()

Some files were not shown because too many files have changed in this diff Show More