Lightweight router for building HTTP services.
Project description
muxy
muxy is a lightweight router for building HTTP services conforming to
Granian's Rust Server Gateway Interface (RSGI). It intentionally avoids magic,
prioritising explicit and composable code.
uv add muxy
Features
- first-class router composition - modularise your code by nesting routers with no overhead
- correct, efficient routing - explicit route heirarchy so behaviour is always predictable
- lightweight - the core router is little more than a simple datastructure and has no dependencies
- control - control the full HTTP request/response cycle without digging through framework layers
- middleware - apply common logic to path groups simply and clearly
Inspiration
Go's net/http and go-chi/chi are inspirations for muxy. I wanted their simplicity
without having to switch language. You can think of the RSGI interface as the muxy
equivalent of the net/http HandlerFunc interface, and muxy.Router as an equivalent of
chi's Mux.
Examples
Getting started
import asyncio
import uvloop
from granian.server.embed import Server
from muxy import Router
from muxy.rsgi import HTTPProtocol, HTTPScope
async def home(s: HTTPScope, p: HTTPProtocol) -> None:
p.response_str(200, [], "Hello world!")
async def main() -> None:
router = Router()
router.get("/", home)
server = Server(router)
try:
await server.serve()
except asyncio.CancelledError:
await server.shutdown()
if __name__ == "__main__":
uvloop.run(main())
Bigger app
See examples/server.py for a runnable script.
import asyncio
import json
import sqlite3
from json.decoder import JSONDecodeError
import uvloop
from granian.server.embed import Server
from muxy import Router, path_params
from muxy.rsgi import HTTPProtocol, HTTPScope, RSGIHTTPHandler
async def main() -> None:
db = sqlite3.connect(":memory:")
router = Router()
router.not_found(not_found)
router.method_not_allowed(method_not_allowed)
router.get("/", home)
router.mount("/user", user_router(db))
router.finalize()
server = Server(router)
try:
await server.serve()
except asyncio.CancelledError:
await server.shutdown()
async def not_found(_scope: HTTPScope, proto: HTTPProtocol) -> None:
proto.response_str(404, [("Content-Type", "text/plain")], "Not found")
async def method_not_allowed(_scope: HTTPScope, proto: HTTPProtocol) -> None:
proto.response_str(405, [("Content-Type", "text/plain")], "Method not allowed")
async def home(s: HTTPScope, p: HTTPProtocol) -> None:
p.response_str(200, [("Content-Type", "text/plain")], "Welcome home")
def user_router(db: sqlite3.Connection) -> Router:
router = Router()
router.get("/", get_users(db))
router.get("/{id}", get_user(db))
router.post("/", create_user(db))
router.patch("/{id}", update_user(db))
return router
def get_users(db: sqlite3.Connection) -> RSGIHTTPHandler:
# closure over handler function to make db available within the handler
async def handler(s: HTTPScope, p: HTTPProtocol) -> None:
cur = db.cursor()
cur.execute("SELECT * FROM user")
result = cur.fetchall()
serialized = json.dumps([{"id": row[0], "name": row[1]} for row in result])
p.response_str(200, [], serialized)
return handler
def get_user(db: sqlite3.Connection) -> RSGIHTTPHandler:
async def handler(s: Scope, p: HTTPProtocol) -> None:
cur = db.cursor()
user_id = path_params.get()["id"]
try:
user_id = int(user_id)
except ValueError:
p.response_str(404, [("Content-Type", "text/plain")], "Not found")
return
cur.execute("SELECT * FROM user WHERE id = ?", (user_id,))
result = cur.fetchone()
if result is None:
p.response_str(404, [("Content-Type", "text/plain")], "Not found")
return
serialized = json.dumps({"id": result[0], "name": result[1]})
p.response_str(200, [("Content-Type", "application/json")], serialized)
return handler
def create_user(db: sqlite3.Connection) -> RSGIHTTPHandler:
async def handler(s: HTTPScope, p: HTTPProtocol) -> None:
cur = db.cursor()
body = await p()
try:
payload = json.loads(body)
except JSONDecodeError:
p.response_str(422, [("Content-Type", "text/plain")], "Invalid json")
return
try:
name = payload["name"]
except KeyError:
p.response_str(422, [("Content-Type", "text/plain")], "No name key")
return
cur.execute("INSERT INTO user (name) VALUES (?) RETURNING *", (name,))
result = cur.fetchone()
serialized = json.dumps({"id": result[0], "name": result[1]})
p.response_str(201, [("Content-Type", "application/json")], serialized)
return handler
def update_user(db: sqlite3.Connection) -> RSGIHTTPHandler: ...
if __name__ == "__main__":
uvloop.run(main())
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file muxy-0.1.0a11.tar.gz.
File metadata
- Download URL: muxy-0.1.0a11.tar.gz
- Upload date:
- Size: 21.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e00485355e5993ff9962072c05e9630b027c85603ffebeb5760fc6eb3beb5ee2
|
|
| MD5 |
34b01aefec286197cf1c7c1322fd39c5
|
|
| BLAKE2b-256 |
d19d28d308b7a2842efdad0c5f2befca27f2857907bc700cae789f01893b9609
|
Provenance
The following attestation bundles were made for muxy-0.1.0a11.tar.gz:
Publisher:
release.yaml on oliverlambson/muxy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
muxy-0.1.0a11.tar.gz -
Subject digest:
e00485355e5993ff9962072c05e9630b027c85603ffebeb5760fc6eb3beb5ee2 - Sigstore transparency entry: 849989137
- Sigstore integration time:
-
Permalink:
oliverlambson/muxy@4d4ce03811b422d5571da2529c2b60c20ca7c80b -
Branch / Tag:
refs/tags/0.1.0a11 - Owner: https://github.com/oliverlambson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@4d4ce03811b422d5571da2529c2b60c20ca7c80b -
Trigger Event:
push
-
Statement type:
File details
Details for the file muxy-0.1.0a11-py3-none-any.whl.
File metadata
- Download URL: muxy-0.1.0a11-py3-none-any.whl
- Upload date:
- Size: 26.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d65a7e11959cd42180e820d99c6f97d2cc95c3ae2cc56ec63a1a52634d7688c
|
|
| MD5 |
30c95227e14846587b1970a06fdcfd1c
|
|
| BLAKE2b-256 |
8d1a030adb7e1cd4a0e286510c070bd523b2d32c2f3b398939820304171e04e6
|
Provenance
The following attestation bundles were made for muxy-0.1.0a11-py3-none-any.whl:
Publisher:
release.yaml on oliverlambson/muxy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
muxy-0.1.0a11-py3-none-any.whl -
Subject digest:
8d65a7e11959cd42180e820d99c6f97d2cc95c3ae2cc56ec63a1a52634d7688c - Sigstore transparency entry: 849989140
- Sigstore integration time:
-
Permalink:
oliverlambson/muxy@4d4ce03811b422d5571da2529c2b60c20ca7c80b -
Branch / Tag:
refs/tags/0.1.0a11 - Owner: https://github.com/oliverlambson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@4d4ce03811b422d5571da2529c2b60c20ca7c80b -
Trigger Event:
push
-
Statement type: