A lightweight, Flask-inspired web framework for Python.
Project description
๐ฉฒ Python In Underwear (PIU)
A lightweight, Flask-inspired web framework for Python. Sync & async, no fluff.
What is PIU?
Python In Underwear is a minimal web framework built for developers who want Flask-like simplicity with modern Python support โ native async/await, WSGI and ASGI interfaces, sessions, auth, rate limiting, WebSockets, OpenAPI docs, and more.
No magic. No bloat. Zero required dependencies.
Features
- Routing โ Decorator-based URL rules with dynamic path parameters (
/user/<id>) - Blueprints โ Group routes into reusable modules with URL prefixes
- Request & Response โ Clean wrappers with JSON, form, cookie, and redirect support
- Middleware โ Chainable
next-style stack, sync and async compatible - Sessions โ HMAC-signed cookie-based sessions, no server-side storage needed
- CSRF Protection โ Token-based CSRF middleware with form and header support
- Rate Limiting โ Global and per-route sliding window rate limiting
- Auth โ
@require_authdecorator with role-based access control - Templates โ Jinja2 integration with autoescaping out of the box
- Static Files โ Automatic static file serving from a configurable directory
- Config โ Load config from dict,
.env, YAML, or environment variables - Hot Reload โ File watcher restarts the server on code changes
- Test Client โ In-process HTTP client with cookie jar, no server needed
- Plugins โ
app.register_plugin()API for modular extensions - Background Tasks โ Fire-and-forget async/sync tasks from within handlers
- WebSockets โ
@app.ws()decorator over the ASGI interface - OpenAPI / Swagger โ Auto-generated docs served at
/docs - WSGI & ASGI โ Deploy with Gunicorn, uWSGI, Uvicorn, or Hypercorn
- CLI โ
piu newandpiu runcommands
Installation
git clone https://github.com/TodorW/PythonInUnderwear.git
cd PythonInUnderwear
pip install -e ".[full]"
Or install extras individually:
pip install -e ".[templates]" # jinja2
pip install -e ".[dev]" # jinja2 + watchdog + pytest
pip install -e ".[full]" # everything
Quickstart
from piu import PIU, Request, Response
app = PIU()
@app.get("/")
def index(request: Request):
return Response(body="<h1>Hello from PIU ๐ฉฒ</h1>")
@app.get("/hello/<name>")
async def hello(request: Request, name: str):
return Response.json({"message": f"Hello, {name}!"})
@app.post("/echo")
def echo(request: Request):
return Response.json({"you_sent": request.json()})
if __name__ == "__main__":
app.run()
CLI
piu new myapp # scaffold a new project
piu run # run the dev server
piu run --reload # run with hot reload
If piu isn't on PATH, use:
python -m piu new myapp
python -m piu run --reload
Routing
@app.get("/users")
@app.post("/users")
@app.put("/users/<id>")
@app.patch("/users/<id>")
@app.delete("/users/<id>")
# Or explicitly:
@app.route("/users", methods=["GET", "POST"])
def users(request):
...
Blueprints
from piu import Blueprint
api = Blueprint("api", prefix="/api")
@api.get("/users")
def users(request):
return Response.json([{"id": 1}])
app.register(api)
# or override prefix:
app.register(api, prefix="/v2")
Request
request.method # "GET", "POST", etc.
request.path # "/hello/world"
request.headers # dict of headers
request.query_params # parsed query string dict
request.body # raw bytes
request.cookies # dict of incoming cookies
request.session # session dict (requires SessionMiddleware)
request.csrf_token # current CSRF token (requires CSRFMiddleware)
request.background_tasks # BackgroundTasks instance
request.json() # parsed JSON body
request.form() # parsed form body
Response
return Response(body="<h1>Hello</h1>")
return Response.json({"key": "value"})
return Response(body="Created", status=201)
return Response.redirect("/new-location")
return Response(body="OK", headers={"X-Custom": "value"})
# Cookies
resp = Response(body="ok")
resp.set_cookie("token", "abc123", max_age=3600, httponly=True)
resp.delete_cookie("token")
Middleware
def logger(request, next):
print(f"{request.method} {request.path}")
return next(request)
async def auth_check(request, next):
if "Authorization" not in request.headers:
return Response(body="Unauthorized", status=401)
return await next(request)
app.middleware.use(logger)
app.middleware.use(auth_check)
Sessions
from piu import SessionMiddleware
app.middleware.use(SessionMiddleware(secret_key="your-secret", max_age=3600))
@app.get("/set")
def set_session(request):
request.session["user"] = "alice"
return Response(body="ok")
@app.get("/get")
def get_session(request):
return Response.json({"user": request.session.get("user")})
CSRF Protection
from piu import CSRFMiddleware
app.middleware.use(CSRFMiddleware(exempt_paths=["/api/"]))
# In your form template:
# <input type="hidden" name="_csrf_token" value="{{ request.csrf_token }}">
# Or in JS via header:
# X-CSRF-Token: <token>
Rate Limiting
from piu import RateLimitMiddleware, rate_limit
# Global: 100 req/min per IP
app.middleware.use(RateLimitMiddleware(limit=100, window=60))
# Per-route
@app.post("/login")
@rate_limit(limit=5, window=60)
def login(request):
...
Auth
from piu import require_auth, login_user, logout_user, current_user
@app.post("/login")
def login(request):
login_user(request, {"id": 1, "role": "admin"})
return Response.redirect("/dashboard")
@app.get("/dashboard")
@require_auth(redirect_to="/login")
def dashboard(request):
user = current_user(request)
return Response(body=f"Hello {user['id']}")
@app.get("/admin")
@require_auth(role="admin", redirect_to="/login")
def admin(request):
...
@app.get("/logout")
def logout(request):
logout_user(request)
return Response.redirect("/login")
Templates
app = PIU(template_dir="templates")
@app.get("/page")
def page(request):
return app.render("index.html", title="Home", user="Alice")
<!-- templates/index.html -->
<h1>{{ title }}</h1>
<p>Welcome, {{ user }}!</p>
Config
app.config.from_env_file(".env") # load from .env
app.config.from_yaml("config.yaml") # load from YAML
app.config.from_dict({"DEBUG": True}) # load from dict
app.config.load_env(prefix="PIU_") # load PIU_* env vars
app.config["SECRET_KEY"] = "abc"
val = app.config.get("PORT", 5000)
Background Tasks
async def send_email(to: str):
...
@app.post("/register")
def register(request):
request.background_tasks.add(send_email, "user@example.com")
return Response(body="registered", status=201)
Plugins
from piu import Plugin
class HealthPlugin(Plugin):
name = "health"
def setup(self, app):
@app.get("/health")
def health(req):
return Response.json({"status": "ok"})
app.register_plugin(HealthPlugin())
WebSockets
Requires Uvicorn (pip install uvicorn):
from piu import WebSocket
@app.ws("/ws/echo")
async def echo(ws: WebSocket):
while True:
msg = await ws.receive_text()
if msg is None:
break
await ws.send_text(f"echo: {msg}")
uvicorn app:app
OpenAPI / Swagger
app.enable_docs(title="My API")
# Swagger UI โ http://127.0.0.1:5000/docs
# Raw schema โ http://127.0.0.1:5000/openapi.json
Testing
from piu.testing import TestClient
from app import app
client = TestClient(app)
def test_index():
resp = client.get("/")
assert resp.status == 200
def test_json():
resp = client.post("/echo", json={"hello": "world"})
assert resp.json() == {"hello": "world"}
pytest tests/ -v
Deployment
WSGI (Gunicorn)
gunicorn "app:app.wsgi"
ASGI (Uvicorn)
uvicorn app:app
Project Structure
piu/
โโโ __init__.py # Public API & version
โโโ __main__.py # python -m piu entry point
โโโ app.py # Core application class
โโโ auth.py # @require_auth, login/logout helpers
โโโ cli.py # CLI commands
โโโ config.py # Config management
โโโ csrf.py # CSRF middleware
โโโ helpers.py # HTTP status utilities
โโโ middleware.py # MiddlewareStack
โโโ openapi.py # OpenAPI schema + Swagger UI
โโโ plugins.py # Plugin base class
โโโ ratelimit.py # Rate limiting middleware & decorator
โโโ routing.py # Route, Router & Blueprint
โโโ serving.py # Dev server & hot reload
โโโ sessions.py # Session middleware
โโโ static.py # Static file serving
โโโ tasks.py # Background tasks
โโโ templating.py # Jinja2 TemplateEngine
โโโ testing.py # TestClient
โโโ websocket.py # WebSocket support
โโโ wrappers.py # Request & Response
License
MIT โ do whatever you want with it.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file python_in_underwear-0.5.0.tar.gz.
File metadata
- Download URL: python_in_underwear-0.5.0.tar.gz
- Upload date:
- Size: 26.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4fbe678a8cf3e437b3f3896c1ec05731675eb9b8f227cf5686e2f7421c32721
|
|
| MD5 |
bc6a907ba9db496659a8386021ea9acc
|
|
| BLAKE2b-256 |
aabb08d732a038b4f220eb2741c49b5af3bc069189f53aaaedf18db6310dfb25
|
File details
Details for the file python_in_underwear-0.5.0-py3-none-any.whl.
File metadata
- Download URL: python_in_underwear-0.5.0-py3-none-any.whl
- Upload date:
- Size: 27.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b88809c661488ecf834c5bd35107c9c61ba13ebd16d27adfe1b3855bb46e947f
|
|
| MD5 |
44b4ac97abbaa7cb67609aa5fc1d31a1
|
|
| BLAKE2b-256 |
461ab99fcda879018c14d3c3e856b0dc39ba553e97fe8347c1d7b34f42bc3397
|