init mini
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
# 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
|
||||
<h1>Hello {{name}}</h1>
|
||||
```
|
||||
```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()
|
||||
```
|
||||
Reference in New Issue
Block a user