Skip to main content

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.
  • 💣 Fail exception – clean non‑200 responses with a single raise.
  • 🔌 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:8000Hello, 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)data can be a string, bytes, or dict (JSON).
  • If data is a dict, it’s auto‑serialised to JSON and the Content‑Type becomes application/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.Logger to Server(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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kissapi-0.1.6.tar.gz (48.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kissapi-0.1.6-py3-none-any.whl (10.8 kB view details)

Uploaded Python 3

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

Hashes for kissapi-0.1.6.tar.gz
Algorithm Hash digest
SHA256 dc2b9200f841643a4adfd226ffb22a9941a4828411c4da0edd1954e50b3d9c9e
MD5 e7ba2568eb9893aac609986f19d80471
BLAKE2b-256 77a40e6f63970f4312325a32f033ec0d81c3d7757eda02e1f3a102b7753cc080

See more details on using hashes here.

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

Hashes for kissapi-0.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 041012d9aa95e027e6e52404723ec71cbbb6b9807ceb23813e523f10d25f08e7
MD5 42f1aec1c191cb4d0e054c9fc740c182
BLAKE2b-256 8b3d9a7c6521e3e9c8d77fe1718528cb8b9322c0de340b677ec39cc130de98be

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page