mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30801557a3 | |||
| 73d46e9b03 | |||
| 3d65d88ea9 | |||
| 8f979719a0 |
@@ -14,13 +14,8 @@ concurrency:
|
||||
jobs:
|
||||
|
||||
documentation:
|
||||
name: "Documentation: Python ${{ matrix.python-version }} on ${{ matrix.os }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest"]
|
||||
python-version: ["3.13"]
|
||||
name: "Documentation"
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
UV_SYSTEM_PYTHON: true
|
||||
|
||||
@@ -30,25 +25,18 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
python-version: "3.13"
|
||||
|
||||
- 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 and documentation dependencies
|
||||
run: |
|
||||
uv pip install '.[develop,docs]'
|
||||
|
||||
- name: Run link checker
|
||||
run: |
|
||||
poe docs-linkcheck
|
||||
run: uv pip install '.[docs]'
|
||||
|
||||
- name: Build static HTML documentation
|
||||
run: |
|
||||
poe docs-html
|
||||
run: sphinx-build -W --keep-going docs/source docs/build
|
||||
|
||||
@@ -49,7 +49,8 @@ jobs:
|
||||
cache-dependency-glob: |
|
||||
pyproject.toml
|
||||
|
||||
- name: Install and validate package
|
||||
run: |
|
||||
uv pip install '.[develop,test]'
|
||||
poe check
|
||||
- name: Install package
|
||||
run: uv pip install '.[develop,test]'
|
||||
|
||||
- name: Run tests
|
||||
run: pytest
|
||||
|
||||
@@ -1,97 +1,109 @@
|
||||
# Responder: a familiar HTTP Service Framework for Python
|
||||
# Responder
|
||||
|
||||
[](https://github.com/kennethreitz/responder/actions/workflows/test.yaml)
|
||||
[](https://github.com/kennethreitz/responder/actions/workflows/docs.yaml)
|
||||
[](https://responder.kennethreitz.org/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pepy.tech/project/responder)
|
||||
[](https://github.com/kennethreitz/responder/graphs/contributors)
|
||||
[](https://pypi.org/project/responder/)
|
||||
A familiar HTTP Service Framework for Python, powered by [Starlette](https://www.starlette.io/).
|
||||
|
||||
[](https://responder.readthedocs.io)
|
||||
```python
|
||||
import responder
|
||||
|
||||
Responder is powered by [Starlette](https://www.starlette.io/).
|
||||
[View documentation](https://responder.readthedocs.io).
|
||||
api = responder.API()
|
||||
|
||||
Responder gets you an ASGI app, with a production static files server pre-installed,
|
||||
Jinja templating, and a production webserver based on uvloop, automatically serving
|
||||
up requests with gzip compression.
|
||||
The `async` declaration within the example program is optional.
|
||||
@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://x.com/kennethreitz42) 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.9+.
|
||||
|
||||
> "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of
|
||||
> [Two Scoops of Django](https://www.feldroy.com/two-scoops-press#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/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"
|
||||
|
||||
Alternatively, 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)}
|
||||
|
||||
Responder supports **Python 3.9+**.
|
||||
# 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 from both Flask and Falcon and
|
||||
unify them into a single framework. You'll find a familiar API with a clean,
|
||||
Pythonic design.
|
||||
api = responder.API(lifespan=lifespan)
|
||||
|
||||
- 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).
|
||||
- Setting `resp.content` sends back bytes.
|
||||
- Use `resp.file("path")` to serve files with automatic content-type detection.
|
||||
- Case-insensitive `req.headers` dict.
|
||||
- `resp.status_code`, `req.method`, `req.url`, and other familiar friends.
|
||||
# GraphQL
|
||||
import graphene
|
||||
api.graphql("/graphql", schema=graphene.Schema(query=Query))
|
||||
|
||||
## Features
|
||||
# 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 expressions with f-string syntax and type convertors
|
||||
(`str`, `int`, `float`, `uuid`, `path`).
|
||||
- HTTP method filtering: `@api.route("/data", methods=["GET"])`.
|
||||
- Every request and response is passed into each view and mutated — including
|
||||
`response.media` for JSON/YAML content negotiation.
|
||||
- Built-in test client powered by Starlette's TestClient.
|
||||
- Mount other WSGI/ASGI apps at subroutes.
|
||||
- Automatic gzip compression.
|
||||
- Class-based views with `on_get`, `on_post`, `on_request` methods.
|
||||
- GraphQL support via Graphene with `api.graphql()`.
|
||||
- OpenAPI schema generation with interactive docs.
|
||||
- Lifespan context managers for startup/shutdown.
|
||||
- Custom exception handlers.
|
||||
- Before-request hooks with short-circuit support.
|
||||
- Cookie-based sessions.
|
||||
- WebSocket support.
|
||||
- Background tasks.
|
||||
- Production uvicorn server built-in.
|
||||
# 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](https://responder.kennethreitz.org/sandbox.html).
|
||||
Built-in OpenAPI docs, cookie-based sessions, gzip compression, static file serving, Jinja2 templates, and a production uvicorn server.
|
||||
|
||||
## Supported by
|
||||
Route convertors: `str`, `int`, `float`, `uuid`, `path`.
|
||||
|
||||
[](https://jb.gg/OpenSourceSupport)
|
||||
## Documentation
|
||||
|
||||
Special thanks to the kind people at JetBrains s.r.o. for supporting us with
|
||||
excellent development tooling.
|
||||
https://responder.kennethreitz.org
|
||||
|
||||
@@ -12,24 +12,25 @@ uv venv
|
||||
```
|
||||
|
||||
Install project in editable mode, including
|
||||
all runtime extensions and development tools.
|
||||
all development tools.
|
||||
```shell
|
||||
uv pip install --upgrade --editable '.[develop,docs,release,test]'
|
||||
```
|
||||
|
||||
## Operations
|
||||
Invoke linter and software tests.
|
||||
Run tests.
|
||||
```shell
|
||||
source .venv/bin/activate
|
||||
poe check
|
||||
pytest
|
||||
```
|
||||
|
||||
Format code.
|
||||
```shell
|
||||
poe format
|
||||
ruff format .
|
||||
ruff check --fix .
|
||||
```
|
||||
|
||||
Documentation authoring.
|
||||
```shell
|
||||
poe docs-autobuild
|
||||
sphinx-autobuild --open-browser --watch docs/source docs/source docs/build
|
||||
```
|
||||
|
||||
@@ -46,7 +46,6 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
develop = [
|
||||
"poethepoet",
|
||||
"pyproject-fmt",
|
||||
"ruff",
|
||||
"validate-pyproject",
|
||||
@@ -182,49 +181,3 @@ implicit_optional = true
|
||||
install_types = true
|
||||
namespace_packages = true
|
||||
non_interactive = true
|
||||
|
||||
[tool.poe.tasks]
|
||||
|
||||
check = [
|
||||
"test",
|
||||
]
|
||||
|
||||
docs-autobuild = [
|
||||
{ cmd = "sphinx-autobuild --open-browser --watch docs/source docs/source docs/build" },
|
||||
]
|
||||
docs-html = [
|
||||
{ cmd = "sphinx-build -W --keep-going docs/source docs/build" },
|
||||
]
|
||||
docs-linkcheck = [
|
||||
{ cmd = "sphinx-build -W --keep-going -b linkcheck docs/source docs/build" },
|
||||
]
|
||||
|
||||
format = [
|
||||
{ cmd = "ruff format ." },
|
||||
# Configure Ruff not to auto-fix (remove!):
|
||||
# unused imports (F401), unused variables (F841), `print` statements (T201), and commented-out code (ERA001).
|
||||
{ cmd = "ruff check --fix --ignore=ERA --ignore=F401 --ignore=F841 --ignore=T20 --ignore=ERA001 ." },
|
||||
{ cmd = "pyproject-fmt --keep-full-version pyproject.toml" },
|
||||
]
|
||||
|
||||
lint = [
|
||||
{ cmd = "ruff format --check ." },
|
||||
{ cmd = "ruff check ." },
|
||||
{ cmd = "validate-pyproject pyproject.toml" },
|
||||
{ cmd = "mypy" },
|
||||
]
|
||||
|
||||
release = [
|
||||
{ cmd = "python -m build" },
|
||||
{ cmd = "twine upload --skip-existing dist/*" },
|
||||
]
|
||||
|
||||
[tool.poe.tasks.test]
|
||||
cmd = "pytest"
|
||||
help = "Invoke software tests"
|
||||
|
||||
[tool.poe.tasks.test.args.expression]
|
||||
options = [ "-k" ]
|
||||
|
||||
[tool.poe.tasks.test.args.marker]
|
||||
options = [ "-m" ]
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.0.0"
|
||||
__version__ = "3.1.0"
|
||||
|
||||
Reference in New Issue
Block a user