Skip to main content

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.

Version Python License


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_auth decorator 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 new and piu run commands

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

python_in_underwear-0.6.0.tar.gz (32.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

python_in_underwear-0.6.0-py3-none-any.whl (34.8 kB view details)

Uploaded Python 3

File details

Details for the file python_in_underwear-0.6.0.tar.gz.

File metadata

  • Download URL: python_in_underwear-0.6.0.tar.gz
  • Upload date:
  • Size: 32.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.12

File hashes

Hashes for python_in_underwear-0.6.0.tar.gz
Algorithm Hash digest
SHA256 7a2700449ee06d9c00b36663c9ba6754c4545c4b89d4456399696d5092c26f89
MD5 efa4c6fdaac3e17dc535bdc51e0c9d2d
BLAKE2b-256 326e7c5bad7f087446ab3b732ad1531f5ee165b86facbe6a208d63885d7e4b39

See more details on using hashes here.

File details

Details for the file python_in_underwear-0.6.0-py3-none-any.whl.

File metadata

File hashes

Hashes for python_in_underwear-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bc7eecab9cf5ec06ffa73171240f70df2a22968fd7cf70e541e413fa59a228c2
MD5 ad67d76a6a64c8e62d8ee548ea18e4dc
BLAKE2b-256 cf31a93f4fbb8e00983a642268c6d75222c5fe8dd99cf25023a90c55634d33a3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page