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.5.0.tar.gz (26.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.5.0-py3-none-any.whl (27.5 kB view details)

Uploaded Python 3

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

Hashes for python_in_underwear-0.5.0.tar.gz
Algorithm Hash digest
SHA256 f4fbe678a8cf3e437b3f3896c1ec05731675eb9b8f227cf5686e2f7421c32721
MD5 bc6a907ba9db496659a8386021ea9acc
BLAKE2b-256 aabb08d732a038b4f220eb2741c49b5af3bc069189f53aaaedf18db6310dfb25

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for python_in_underwear-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b88809c661488ecf834c5bd35107c9c61ba13ebd16d27adfe1b3855bb46e947f
MD5 44b4ac97abbaa7cb67609aa5fc1d31a1
BLAKE2b-256 461ab99fcda879018c14d3c3e856b0dc39ba553e97fe8347c1d7b34f42bc3397

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