Metadata-Version: 2.4
Name: kissapi
Version: 0.1.6
Summary: Pure Python 3 async TCP & HTTP server framework following the KISS philosophy.
Project-URL: Homepage, https://gitlab.com/viraven/python-api
Project-URL: Repository, https://gitlab.com/viraven/python-api
Author-email: Ivan Chetchasov <vi.is.chapmann@gmail.com>
License: MIT
License-File: LICENSE
Keywords: access-control,libcan,rbac,security
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Requires-Dist: twine>=6.2.0
Description-Content-Type: text/markdown

# 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

```bash
pip install kissapi
```

Or with [uv](https://github.com/astral-sh/uv):

```bash
uv pip install kissapi
```

---

## 🏁 Quick start

```python
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

```python
@router.get("/about")
async def about(req: Request):
    return "About us"
```

### Path parameters

```python
@router.get("/users/{id}")
async def get_user(req: Request):
    user_id = req.params["id"]
    return f"User {user_id}"
```

### Sub‑routers (mounting)

```python
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

```python
@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:

```python
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:

```python
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.

```python
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.

```python
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:

```bash
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](https://gitlab.com/viraven/python-api/-/tree/main/examples).

---

## 🧪 Running tests

```bash
uv run pytest tests/
```

---

## 📜 License

MIT – see the [LICENSE](https://gitlab.com/viraven/python-api/-/blob/main/LICENSE) file.

---

## 🤝 Contributing

Bug reports, feature requests, and pull requests are welcome on the [GitLab repository](https://gitlab.com/viraven/python-api).

Keep it simple. Keep it Pythonic.
