Pure Python 3 async TCP & HTTP server framework following the KISS philosophy.
Project description
KISSAPI
Pure Python 3 async TCP & HTTP server framework following the KISS philosophy.
Zero‑dependency (only httptools for fast HTTP parsing), modular, and ridiculously simple.
Build your own microservices, APIs, or custom protocol servers in minutes – not hours.
✨ Features
- 🚀 Async TCP server – high‑performance with
asyncio. - 🧠 Automatic HTTP parsing – method, path, query params, headers, body.
- 🧩 Flexible routing – exact paths, path parameters (
/users/{id}), and sub‑routers. - 🪝 Hooks – before/after request middleware.
- 🔁 Keep‑alive – HTTP/1.1 persistent connections out of the box.
- 🌈 Pretty logging – coloured console output with request duration and status.
- 🎨 Auto‑serialisation – return strings, bytes, or dicts (→ JSON) from your handlers.
- 💣
Failexception – clean non‑200 responses with a singleraise. - 🔌 Protocol agnostic – swap the parser to support custom TCP protocols.
- 📦 Tiny footprint – < 300 lines of core logic, installs in seconds.
📦 Installation
pip install kissapi
Or with uv:
uv pip install kissapi
🏁 Quick start
from kissapi import Server, Router, Request
router = Router()
@router.get("/")
async def hello(req: Request):
return "Hello, world!"
server = Server(router=router)
server.loop() # serves on 0.0.0.0:8000
Access http://localhost:8000 → Hello, world!
🧭 Routing
Exact paths
@router.get("/about")
async def about(req: Request):
return "About us"
Path parameters
@router.get("/users/{id}")
async def get_user(req: Request):
user_id = req.params["id"]
return f"User {user_id}"
Sub‑routers (mounting)
api_v1 = Router()
@api_v1.get("/")
async def api_home(req: Request):
return {"version": "1.0"} # auto‑JSON
root = Router()
root["/api/v1"] = api_v1 # mount the sub‑router
Now /api/v1 returns JSON.
Multiple HTTP methods
@router.post("/submit")
async def submit(req: Request):
data = req.text
return f"Received: {data}"
@router.get("/submit")
async def submit_form(req: Request):
return "Send POST to submit"
📬 Request object
Every handler receives a Request instance:
req.method # "GET", "POST", ...
req.path # "/users/42" (without query string)
req.query # {"page": "1"} (dict of first values)
req.params # {"id": "42"} (path parameters)
req.headers # {"host": "example.com", ...}
req.body # raw bytes
req.text # body decoded as UTF-8
req.http_version # "1.1"
📤 Response
Handlers can return three types, all automatically handled:
| Return type | Content‑Type | Example |
|---|---|---|
str |
text/plain |
"hello" → sent as UTF-8 |
bytes |
text/plain |
b"hello" → sent as‑is |
dict, list, etc. |
application/json |
{"msg": "hi"} → JSON |
🧨 Error handling with Fail
Throw a Fail exception anywhere (handler, hook, router) to immediately send a custom status code:
from kissapi import Fail
@router.get("/secret")
async def secret(req: Request):
if "authorization" not in req.headers:
raise Fail(403, "Forbidden")
return "Top secret"
Fail(status_code, data)–datacan be a string, bytes, or dict (JSON).- If
datais a dict, it’s auto‑serialised to JSON and theContent‑Typebecomesapplication/json.
🪝 Hooks (middleware)
Add before/after hooks to any router.
async def log_request(req: Request):
print(f"--> {req.method} {req.path}")
async def add_custom_header(req: Request, resp: object) -> object:
# resp is whatever the handler returned (or modified by previous hooks)
# you can modify it, wrap it, etc.
if isinstance(resp, dict):
resp["hooked"] = True
return resp
router.add_hook("before", log_request)
router.add_hook("after", add_custom_header)
- before hooks – run before the handler, can inspect/modify the request, raise
Fail. - after hooks – run after the handler, can inspect/modify the response.
Hooks are router‑specific; a hook added to a parent router runs for that router’s exact routes, while hooks on a sub‑router run only for routes inside that sub‑router.
🎨 Pretty logging
No configuration needed – every server logs to stdout with colours:
12:34:56 [INFO] Serving on http://0.0.0.0:8000
12:35:01 [INFO] GET / → 200 14B 0.2ms
12:35:05 [INFO] GET /users/5 → 200 24B 0.3ms
12:35:09 [INFO] POST /submit → 404 9B 0.1ms
- Status codes are coloured: 2xx green, 3xx cyan, 4xx yellow, 5xx red.
- You can pass your own
logging.LoggertoServer(logger=my_logger).
🔌 Custom protocol / parser
Want to serve a different protocol (e.g., a custom binary protocol or line‑based RPC)? Subclass Request and provide your own parser.
from kissapi import Server, Router, Request
class MyParser: # must have __init__(self, request) and feed_data(data)
def __init__(self, request: Request):
self.request = request
self._finished = False
self._buffer = b""
def feed_data(self, data: bytes):
self._buffer += data
if b"\r\n\r\n" in self._buffer:
header_part, body = self._buffer.split(b"\r\n\r\n", 1)
lines = header_part.split(b"\r\n")
command, path = lines[0].split(b" ", 1)
self.request.method = command.decode()
self.request.path = path.decode()
# parse headers, etc.
self._finished = True
server = Server(parser_class=MyParser, router=router)
server.loop()
The routing, hooks, and response conversion remain unchanged.
📚 Examples
Run the examples from the examples/ folder after installing the package:
python -m examples.subrouters # sub‑routers & Fail exception
python -m examples.extended # path parameters, hooks
python -m examples.custom_proto # custom protocol parser
Find them in the repository.
🧪 Running tests
uv run pytest tests/
📜 License
MIT – see the LICENSE file.
🤝 Contributing
Bug reports, feature requests, and pull requests are welcome on the GitLab repository.
Keep it simple. Keep it Pythonic.
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 kissapi-0.1.6.tar.gz.
File metadata
- Download URL: kissapi-0.1.6.tar.gz
- Upload date:
- Size: 48.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dc2b9200f841643a4adfd226ffb22a9941a4828411c4da0edd1954e50b3d9c9e
|
|
| MD5 |
e7ba2568eb9893aac609986f19d80471
|
|
| BLAKE2b-256 |
77a40e6f63970f4312325a32f033ec0d81c3d7757eda02e1f3a102b7753cc080
|
File details
Details for the file kissapi-0.1.6-py3-none-any.whl.
File metadata
- Download URL: kissapi-0.1.6-py3-none-any.whl
- Upload date:
- Size: 10.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
041012d9aa95e027e6e52404723ec71cbbb6b9807ceb23813e523f10d25f08e7
|
|
| MD5 |
42f1aec1c191cb4d0e054c9fc740c182
|
|
| BLAKE2b-256 |
8b3d9a7c6521e3e9c8d77fe1718528cb8b9322c0de340b677ec39cc130de98be
|