# mini.py - Native Async Python HTTP Server Single-file, plug-and-play, extremely object-oriented server using only stdlib + `asyncio`. Modern Python with `pathlib` and `f-strings`. Run: ```bash python mini.py python mini.py 8000 ``` Data defaults to `./data`. Config in `./.env.json` (port, version, future settings). ## Core Classes - `Mini` - app, router, server. `app.serve(port=None)` auto-picks 22000-24000 remembering last in `.env.json`. - `Request` - `method, path, query, headers, cookies, path_params, query_params, form, body, json`. `req.param(name, default)`, `req.all_params`. - `Response` - `Response.text/html/json/redirect`. Return `dict` -> JSON, `str` -> text, `bytes` -> raw. - `Route` - compiled `{param}` patterns, optional `auth=True`. - `Field` / `Validator` - validation framework. - `Template(dir)` - `render(name, ctx)` with `{{key}}` escaped. - `DB` - async sqlite wrapper. - `Sessions` - cookie `sid` sessions, in-memory cache to 50MB synced to sqlite. - `PubSub` - builtin `sub/unsub/pub` with `asyncio.Queue`. - `Time` - `Time.now()` timezone-aware UTC. - `Ids` - `Ids.uuid7()`. - `Esc` - `Esc.x(s)` html-escape. ## Routing ```python app = Mini() app.static() # serve ./static @app.get("/") async def idx(req: Request): return {"ok": True} @app.get("/hi/{name}") def hi(req): # sync -> run in thread like FastAPI return f"hi {req.path_params['name']}" @app.post("/submit") async def sub(req): return {"got": req.all_params} app.route("GET", "/x", handler, auth=True) app.get("/p", handler, auth=False) app.post("/p", handler) ``` URL-decoding automatic via `urllib.parse`. All output escaping via `Esc.x` in templates. ## GET/POST/JSON `query_params` from querystring, `form` from `application/x-www-form-urlencoded` and `multipart` (basic), `json_data` from `application/json` auto-decoded. `param()` checks path > query > form > json. ```python async def h(req): page = req.param("page", "1") return Response.json(req.all_params) ``` ## Validation ```python v = Validator({ "email": Field(required=True, regex=r"^[^@]+@[^@]+$", max_len=200), "age": Field(type_=int, default=18), "name": Field(required=True, min_len=2, max_len=50), }) clean = v.clean(req.all_params) ``` `Field(required, regex, type_, default, min_len, max_len)`. Raises `ValueError(dict)` on fail. Returns 400 automatically if you call in handler? Catch yourself: ```python try: data = v.clean(req.all_params) except ValueError as e: return Response.json({"errors": e.args[0]}, 400) ``` ## Templates `./templates/hello.html`: ```html

Hello {{name}}

``` ```python tpl = Template("templates") @app.get("/t") async def t(req): return tpl.render("hello.html", {"name": req.param("name","world")}) ``` Auto-escaped. ## Static + Versioned URLs + Mime + Video Streaming ```python app.static(prefix="/static", dir="./static") app.static(prefix="/v{version}/static", dir="./static") # header caching, bump version ``` Mime via `mimetypes`. `Range: bytes=` supported for video streaming. Path-traversal blocked via `Path.resolve()` check. `Cache-Control: max-age=31536000` on versioned. ## Base64 Auth ```python @app.get("/priv", auth=True) async def priv(req): return {"user": req.headers.get("authorization")} ``` Set `auth=True` -> requires `Authorization: Basic base64(user:pass)`. Returns 401 otherwise. Check `req.auth_user`, `req.auth_pass` (set by Mini). ## Sessions Automatic cookie `sid`. Access via `req.session` dict (loaded), save automatically at end of request. ```python @app.get("/count") async def c(req): req.session["n"] = req.session.get("n",0)+1 return req.session ``` Cached in memory to 50MB, synced to `sessions` table in sqlite file. ## SQLite + Auto CRUD File `./data/app.db`. Single-writer lock (`asyncio.Lock`), concurrent readers. All datetimes timezone-aware ISO strings. Tables auto-created with: `id INTEGER PK AUTOINCREMENT, uid TEXT(uuid7), created_at TEXT, updated_at TEXT, deleted_at TEXT` Missing columns auto-added then retry (up to 6 tries). Multi-field `WHERE` auto-indexed (`CREATE INDEX IF NOT EXISTS`), cached in `self._idx`. ```python db = app.db await db.insert("users", name="bob", email="b@x.com") await db.update("users", {"email":"b@x.com"}, {"name":"bobby"}) await db.upsert("users", {"email":"b@x.com"}, name="bob") await db.delete("users", {"email":"b@x.com"}) # soft-delete await db.find("users", {"name":"bob"}) await db.insert_many("users", [{"name":"a"},{"name":"b"}]) await db.update_many("users", [({"name":"a"}, {"name":"a2"})]) await db.upsert_many("users", [({"name":"a"}, {"name":"a2"})]) ``` `_later` variants queue and return preview instantly: ```python await db.insert_later("users", name="fast") await db.update_later("users", {"name":"a"}, {"name":"b"}) # also: upsert_later, delete_later, insert_many_later, upsert_many_later, update_many_later ``` Background `drain()` task flushes queue. ## Version / Header Caching ```python app = Mini(version="1.2.3") ``` Use `/v{version}/...` routes. Bump version to bust cache. ## PubSub ```python q = asyncio.Queue() app.pubsub.sub("chat", q) await app.pubsub.pub("chat", {"hi":1}) msg = await q.get() ``` ## Sync handlers in threads If handler is not `async`, executed via `run_in_executor` like FastAPI. ## .env.json ```json {"port": 23000, "version": "1.0.0"} ``` Auto-created. `serve()` prefers saved port, else scans 22000-24000. ## Full Example ```python from mini import Mini, Field, Validator app = Mini() app.static() v = Validator({"name": Field(required=True, min_len=2)}) @app.get("/") async def idx(req): return {"ok": True, "v": app.version} @app.post("/users") async def create(req): try: d = v.clean(req.all_params) except ValueError as e: from mini import Response return Response.json({"errors": e.args[0]}, 400) row = await app.db.insert("users", **d) return row @app.get("/users/{uid}") async def one(req): rows = await app.db.find("users", {"uid": req.path_params["uid"]}) return rows[0] if rows else ({"error":"not found"}, 404) app.serve() ```