Metadata-Version: 2.4
Name: minihttp
Version: 0.2.0
Summary: A deliberately small HTTP server framework for trusted networks and reverse-proxy deployments
Author-email: Mizuki Hikaru <mizuki@hikaru.org>
License: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: relix

# minihttp

minihttp is a small HTTP server framework for Python applications running on trusted networks or behind a more developed web server such as nginx. It provides typed request handlers, JSON serialization, persistent sessions, and a deliberately small HTTP feature set.

## Features

### Creating a server

Create a `Server` and call `run()` to start it.

```python
from minihttp import Server

server = Server()

server.run()
```

By default, the server listens on `127.0.0.1:2000`.

You can also specify a persistent SQLite database:

```python
server = Server("minihttp.db")
```

This initializes relix for the application and also provides storage for sessions.

To listen on a different address:

```python
server.run("0.0.0.0", 8000)
```

---

### Routes

Use method decorators to register handlers.

```python
from minihttp import Server

server = Server()

@server.get("/")
def index():
    return {
        "message": "Hello, world!",
    }

@server.post("/users")
def create_user():
    return {
        "created": True,
    }

server.run()
```

minihttp supports:

```python
@server.get("/example")
@server.post("/example")
@server.put("/example")
@server.patch("/example")
@server.delete("/example")
@server.head("/example")
@server.options("/example")
```

You can also register a method explicitly:

```python
@server.route("GET", "/hello")
def hello():
    return "Hello"
```

---

### Path variables

Prefix part of a route with `:` to create a path variable.

```python
@server.get("/users/:id")
def get_user(id: int):
    return {
        "id": id,
    }
```

A request for:

```text
GET /users/42
```

calls the handler with `id` as the integer `42`.

Multiple path variables can be used:

```python
@server.get("/users/:user_id/posts/:post_id")
def get_post(user_id: int, post_id: int):
    return {
        "user_id": user_id,
        "post_id": post_id,
    }
```

Path variables without a type annotation are passed as strings.

---

### Query parameters

For `GET`, `DELETE`, `HEAD`, and `OPTIONS` requests, a dataclass handler argument is populated from the query string.

```python
from dataclasses import dataclass

@dataclass
class Search:
    query: str
    page: int = 1
    exact: bool = False

@server.get("/search")
def search(options: Search):
    return {
        "query": options.query,
        "page": options.page,
        "exact": options.exact,
    }
```

A request such as:

```text
GET /search?query=python&page=2&exact=true
```

becomes:

```python
Search(
    query="python",
    page=2,
    exact=True,
)
```

Dataclass defaults and `Optional` values can be used normally.

```python
from dataclasses import dataclass
from typing import Optional

@dataclass
class Options:
    page: int = 1
    search: Optional[str] = None
```

A handler may contain at most one dataclass request argument.

---

### JSON request bodies

For `POST`, `PUT`, and `PATCH`, the handler's dataclass argument is populated from the JSON request body.

```python
from dataclasses import dataclass

@dataclass
class NewUser:
    name: str
    email: str
    is_admin: bool = False

@server.post("/users")
def create_user(user: NewUser):
    return {
        "name": user.name,
        "email": user.email,
        "is_admin": user.is_admin,
    }
```

A request containing:

```json
{
    "name": "Alice",
    "email": "alice@example.com"
}
```

is converted into a `NewUser` before the handler is called.

Nested dataclasses are supported:

```python
@dataclass
class Address:
    city: str
    country: str

@dataclass
class NewUser:
    name: str
    address: Address

@server.post("/users")
def create_user(user: NewUser):
    return user
```

Invalid values result in a `400` response.

---

### Request headers

Add a `Headers` argument when a handler needs access to request headers.

```python
from minihttp import Headers

@server.get("/headers")
def show_headers(headers: Headers):
    return {
        "user_agent": headers.get("User-Agent"),
    }
```

Header names are case-insensitive:

```python
headers["Content-Type"]
headers["content-type"]
headers["CONTENT-TYPE"]
```

all refer to the same value.

Headers are stored internally using lowercase names.

A handler may contain at most one `Headers` argument.

---

### Sessions

Add a `Session` argument to a handler to use server-side session data.

```python
from minihttp import Session

@server.get("/login")
def login(session: Session):
    session.username = "alice"

    return {
        "logged_in": True,
    }
```

Session values support both attribute and dictionary access:

```python
session.username = "alice"
session["username"] = "alice"

print(session.username)
print(session["username"])
```

Nested dictionaries also support attribute access:

```python
session.preferences = {
    "theme": "dark",
    "notifications": True,
}

session.preferences.theme = "light"
```

Changes are tracked recursively.

minihttp automatically saves a changed session after the request finishes, so handlers normally do not need to call `session.save()` themselves.

Sessions are persisted using relix.

---

### Session keys

Each session has a cryptographically random key containing 128 bits of entropy.

```python
@server.get("/session")
def show_session(session: Session):
    return {
        "key": session.key,
    }
```

The key is stored in the client's `key` cookie:

```text
key=...
```

If the client does not have a valid session key, minihttp creates a new session with a new random key.

A supplied key that does not exist in the database is never reused to create a session.

Sessions are only loaded when the handler actually declares a `Session` argument.

---

### relix initialization

minihttp can initialize relix automatically.

```python
from minihttp import Server

server = Server("app.db")
```

This is equivalent to using `app.db` as the relix database for models and sessions.

For an in-memory database:

```python
server = Server()
```

Models should be defined before the `Server` is constructed so their tables can be created during database initialization.

relix can also be used independently of minihttp:

```python
from relix import Database

database = Database.init("app.db")
```

---

### Returning JSON

Handlers can return native Python values directly.

```python
@server.get("/status")
def status():
    return {
        "running": True,
        "workers": 4,
    }
```

Lists work too:

```python
@server.get("/numbers")
def numbers():
    return [1, 2, 3]
```

Strings, integers, floats, booleans, and `None` are also serialized as JSON.

---

### Returning dataclasses

Dataclass instances are serialized automatically.

```python
from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: str

@server.get("/user")
def user():
    return User(
        name="Alice",
        email="alice@example.com",
    )
```

relix models are dataclasses too, so they can also be returned directly from handlers.

---

### Custom responses

Return a `Response` when you need control over the status, headers, content type, or raw body.

```python
from minihttp import Response

@server.get("/raw")
def raw():
    return Response(
        b"hello",
        status=200,
        headers={
            "X-Example": "value",
        },
        content_type="application/octet-stream",
    )
```

---

### JSON responses

Use `JSONResponse` when you need JSON with a custom status or headers.

```python
from minihttp import JSONResponse

@server.post("/users")
def create_user():
    return JSONResponse(
        {
            "id": 42,
        },
        status=201,
        headers={
            "X-Created": "yes",
        },
    )
```

---

### Text responses

Use `TextResponse` for plain text.

```python
from minihttp import TextResponse

@server.get("/hello.txt")
def hello():
    return TextResponse("Hello, world!")
```

---

### HTML responses

Use `HTMLResponse` for HTML.

```python
from minihttp import HTMLResponse

@server.get("/")
def index():
    return HTMLResponse("""
        <!doctype html>
        <html>
            <body>
                <h1>Hello!</h1>
            </body>
        </html>
    """)
```

Files can be read normally:

```python
from pathlib import Path
from minihttp import HTMLResponse

@server.get("/")
def index():
    path = Path(__file__).parent / "index.html"
    return HTMLResponse(
        path.read_text(encoding="utf-8")
    )
```

---

### CSS responses

Use `CSSResponse` for stylesheets.

```python
from minihttp import CSSResponse

@server.get("/style.css")
def stylesheet():
    return CSSResponse("""
        body {
            font-family: sans-serif;
        }
    """)
```

---

### JavaScript responses

Use `JavaScriptResponse` for JavaScript.

```python
from minihttp import JavaScriptResponse

@server.get("/app.js")
def javascript():
    return JavaScriptResponse("""
        console.log("Hello from minihttp");
    """)
```

---

### File responses

Use `FileResponse` to send a file.

```python
from pathlib import Path
from minihttp import FileResponse

@server.get("/example.tar.gz")
def download():
    path = Path(__file__).parent / "example.tar.gz"
    return FileResponse(path)
```

The content type is inferred from the filename when possible.

---

### Response headers

Response helpers accept custom headers.

```python
from minihttp import TextResponse

@server.get("/example")
def example():
    return TextResponse(
        "Hello",
        headers={
            "Cache-Control": "no-store",
            "X-Example": "value",
        },
    )
```

---

### Response status codes

Response helpers accept custom status codes.

```python
from minihttp import JSONResponse

@server.post("/users")
def create_user():
    return JSONResponse(
        {
            "id": 42,
        },
        status=201,
    )
```

---

### Combining handler arguments

Path variables, a dataclass, `Headers`, and `Session` can be combined in the same handler.

```python
from dataclasses import dataclass
from minihttp import Headers, Session

@dataclass
class Options:
    include_details: bool = False

@server.get("/users/:id")
def get_user(
    id: int,
    options: Options,
    headers: Headers,
    session: Session,
):
    session.last_user = id

    return {
        "id": id,
        "include_details": options.include_details,
        "user_agent": headers.get("User-Agent"),
    }
```

The handler signature describes exactly which parts of the request the handler needs.

---

### Persistent connections

HTTP/1.1 connections are kept alive by default, allowing multiple requests to use the same TCP connection.

Clients can explicitly close the connection with:

```text
Connection: close
```

HTTP/1.0 connections close by default unless the client requests:

```text
Connection: keep-alive
```

minihttp handles response framing using `Content-Length`, so multiple requests and responses can share the same connection safely.

---

### A small application

minihttp and relix can be used together with very little setup.

```python
from dataclasses import dataclass

from minihttp import JSONResponse, Server, Session
from relix import Model


@dataclass
class User(Model):
    username: str
    display_name: str


@dataclass
class Login:
    username: str


server = Server("app.db")


@server.post("/login")
def login(data: Login, session: Session):
    user = User.where(
        User.username == data.username
    ).first()

    if user is None:
        return JSONResponse(
            {
                "error": "unknown user",
            },
            status=404,
        )

    session.user_id = user.id

    return {
        "logged_in": True,
    }


@server.get("/profile")
def profile(session: Session):
    user_id = session.get("user_id")

    if user_id is None:
        return JSONResponse(
            {
                "error": "not logged in",
            },
            status=401,
        )

    user = User.get(user_id)

    if user is None:
        return JSONResponse(
            {
                "error": "user not found",
            },
            status=404,
        )

    return user


server.run()
```

minihttp deliberately supports only a subset of HTTP. It is intended for trusted networks or deployments where a reverse proxy such as nginx handles the broader public-facing HTTP concerns.
