slick-framework
A slick, minimal ASGI web framework for Python, built only on the standard library.
slick covers what most small services need: routing with typed path parameters,
request and response objects, JSON in and out, middleware, lifespan hooks, an
in-process test client and a development server. It has zero runtime dependencies,
and the core is a few hundred lines you can read in one sitting.
from slick import Slick
app = Slick()
@app.get("/hello/{name}")
async def hello(request, name: str):
return {"message": f"Hello, {name}!"}
Features
- ASGI 3 app: runs under uvicorn, hypercorn or any other ASGI server.
- Routing with
{name},{id:int},{x:float}and{rest:path}parameters, converted values, automaticHEADforGETroutes, and proper404/405(withAllow) responses. - Path params injected as keyword arguments when the handler's signature names them.
- Return plain values:
dict/listbecomes JSON,strbecomes text,Nonebecomes204, and(body, status[, headers])tuples set the status and headers. - Request object with headers, query params, cookies,
await request.json()/.form()/.body(). - Responses:
Response,JSONResponse,PlainTextResponse,HTMLResponse,RedirectResponse, plus cookies. - Middleware: plain
async def mw(request, call_next)functions. - Errors:
HTTPException, plus custom handlers for any exception type. - Lifespan hooks:
@app.on_startup/@app.on_shutdown, andapp.state/request.state. - Sub-routers:
Router()andapp.include_router(router, prefix="/api"). TestClient: synchronous and in-process, with no sockets and nothing extra to install.serve(): a stdlib-only development server, so examples run straight away.- Sync handlers are supported too (they run in a worker thread). Fully type-hinted (
py.typed).
Install
pip install slick-framework # core, no dependencies
pip install "slick-framework[server]" # optional: adds uvicorn for production serving
Requires Python 3.9+.
Quickstart
# app.py
from slick import HTTPException, Request, Slick, serve
app = Slick()
BOOKS = {1: {"id": 1, "title": "Dune"}}
@app.get("/books/{book_id:int}")
async def get_book(request: Request, book_id: int):
if book_id not in BOOKS:
raise HTTPException(404, "No such book")
return BOOKS[book_id]
@app.post("/books")
async def add_book(request: Request):
data = await request.json() # malformed JSON -> 400 automatically
book = {"id": len(BOOKS) + 1, "title": data["title"]}
BOOKS[book["id"]] = book
return book, 201
if __name__ == "__main__":
serve(app, port=8000) # or: uvicorn app:app
Test it without a network:
from slick import TestClient
from app import app
with TestClient(app) as client: # `with` also runs startup/shutdown hooks
assert client.get("/books/1").json() == {"id": 1, "title": "Dune"}
assert client.post("/books", json={"title": "Emma"}).status_code == 201
assert client.get("/books/99").status_code == 404
A fuller example (a todo CRUD API with a sub-router, middleware and startup hook) is in
examples/todo_app.py:
python3 examples/todo_app.py # after `pip install -e .`; or prefix with PYTHONPATH=src
API overview
Everything below is importable from slick.
Slick(*, debug=False)
The application, and an ASGI callable. It subclasses Router, so it has all of the router methods.
| Member | Description |
|---|---|
@app.get(path, name=None), .post, .put, .patch, .delete |
Register a handler for one method. GET also answers HEAD. |
@app.route(path, methods=None, name=None) |
Register for several methods (default ["GET"]). |
app.add_route(path, handler, methods=None, name=None) -> Route |
Non-decorator form. |
app.include_router(router, prefix="") |
Copy a Router's routes in under prefix. |
app.url_for(name, **params) -> str |
Build a path from a route name. The name defaults to the handler's __name__. |
@app.middleware |
Register async def mw(request, call_next) -> Response. The first registered is outermost. |
@app.exception_handler(ExcClass) |
Register handler(request, exc), sync or async, for that class and its subclasses. |
@app.on_startup, @app.on_shutdown |
Lifespan hooks taking no arguments, sync or async. |
app.state |
A State attribute bag for app-wide objects. |
app.debug |
If true, an unhandled error returns its traceback as a 500 text body. |
Handlers are handler(request, **path_params), async def or plain def. A path
param is passed as a keyword argument only if the signature names it or accepts
**kwargs. Otherwise read it from request.path_params. A handler may return:
| Return value | Response |
|---|---|
Response (or subclass) |
sent as-is |
dict / list |
JSONResponse |
str |
PlainTextResponse |
bytes |
Response with application/octet-stream |
None |
empty 204 No Content |
(value, status) / (value, status, headers) |
the above, with that status and extra headers |
Middleware may return the same kinds of value.
Path converters: str (default, one segment), int (optionally negative), float, path (can include /).
Matching is exact, with no trailing-slash redirects. Routes are tried in registration order.
Errors: HTTPException and unmatched routes render as {"detail": ...} JSON, and
middleware sees them as normal responses. Any other uncaught exception becomes a
500 {"detail": "Internal Server Error"}, is logged on the slick logger and is re-raised to the server.
Router()
A collection of routes that has the same get/post/put/patch/delete/route/add_route/include_router/url_for
methods as the app, plus resolve(method, path) -> (Route, params). Route(path, handler, methods=None, name=None)
provides .match(path) and .url_for(**params).
Request
| Member | Description |
|---|---|
method, path, url |
Upper-case method, decoded path, and the reconstructed absolute URL. |
headers |
Case-insensitive Headers. |
query_params |
QueryParams: get(key, default) returns the first value, getlist(key) returns all values. |
path_params |
dict of converted route parameters. |
cookies |
dict parsed from the Cookie header. |
client |
(host, port) or None. |
state |
A per-request State, for example set by middleware. |
app, scope |
The Slick app and the raw ASGI scope. |
await body(), await text() |
The raw body (cached) and the body decoded as UTF-8. |
await json() |
The parsed body. Raises HTTPException(400) on invalid JSON. |
await form() |
A dict from a urlencoded body. |
Responses
Response(content=None, status_code=200, headers=None, media_type=None):contentisbytes,strorNone. It has.status_code,.body, a mutable.headers(Headers) and.set_cookie(key, value="", *, max_age=None, path="/", httponly=False, secure=False, samesite="lax").JSONResponse(content, status_code=200, headers=None, *, indent=None, default=None)produces compact UTF-8 JSON by default.PlainTextResponse,HTMLResponsetake the same arguments asResponse.RedirectResponse(url, status_code=307, headers=None).to_response(value)applies the return-value rules above.
HTTPException(status_code, detail=None, headers=None)
When detail is omitted it defaults to the standard reason phrase (for example "Not Found").
TestClient(app, base_url="http://testserver", raise_server_exceptions=True)
.get/.head/.post/.put/.patch/.delete/.options(url, *, params=None, headers=None, json=None, data=None, cookies=None)and.request(method, url, ...)return aTestResponse.datatakes a form mapping,strorbytes.with TestClient(app) as client:runs the app's startup and shutdown hooks..close()releases the event loop.Set-Cookievalues are stored inclient.cookiesand sent on later requests.- With
raise_server_exceptions=Falseyou get the500response instead of the exception. TestResponsehas.status_code,.headers,.content,.text,.json()and.ok.
serve(app, host="127.0.0.1", port=8000)
A threaded development server built on http.server. It runs lifespan hooks and stops on Ctrl+C.
It buffers whole bodies and is not meant for production; use uvicorn yourmodule:app there.
Data structures
Headers (case-insensitive and multi-valued: getlist, append, multi_items, raw, from_raw),
QueryParams (getlist, multi_items), and State (an attribute bag with .get(name, default)).
Development
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest # or, with no installs at all:
PYTHONPATH=src python3 -m unittest discover -s tests
License
MIT
Metadata
Release files for slick-framework 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| slick_framework-0.1.0.tar.gz | 28.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| slick_framework-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 52.2 kB
Release files / slick_framework-0.1.0.tar.gz
| Download URL | slick_framework-0.1.0.tar.gz |
|---|---|
| Size | 28.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a1528c707621dfe7be385eeb3e89f4ea0d5e024d6c72d626084bda0381dd85c1
|
|
BLAKE2b-256 checksum How to use checksums |
58a27df4ec870d9ca1841562df0944e75868fa14be78b15491cce9828c8c882d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Release files / slick_framework-0.1.0-py3-none-any.whl
| Download URL | slick_framework-0.1.0-py3-none-any.whl |
|---|---|
| Size | 24.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bc2bc50e38be971136770c49eebc54cceabe7df700a6a394f34b98e262603dd7
|
|
BLAKE2b-256 checksum How to use checksums |
fac7ee07cb4c47dafcce4e33720d70ce515d5121386c2d24fdb30e75d05f37a5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Security for slick-framework 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.