Claude Code for Bottle: Single-File Python Micro Web Framework — Claude Skills 360 Blog
Blog / AI / Claude Code for Bottle: Single-File Python Micro Web Framework
AI

Claude Code for Bottle: Single-File Python Micro Web Framework

Published: May 28, 2028
Read time: 5 min read
By: Claude Skills 360

Bottle is a single-file Python micro web framework with no dependencies. pip install bottle. App: from bottle import Bottle; app = Bottle(). Route: @app.route("/hello"); def hello(): return "Hi". GET/POST: @app.get("/users"); @app.post("/users"). Path params: @app.route("/users/<uid:int>"); def user(uid): .... Wildcard: <path:path>. Request: from bottle import request; request.query.name; request.forms.email; request.json. Headers: request.headers["Authorization"]. Cookies: request.get_cookie("session"). Response: from bottle import response; response.content_type = "application/json"; return json.dumps({}). Status: from bottle import abort; abort(404,"Not found"). Redirect: from bottle import redirect; redirect("/login"). HTTPError: from bottle import HTTPError; raise HTTPError(400,"bad input"). Template: from bottle import template; return template("index.html", name=name). Jinja2: from bottle import jinja2_template as template. Static: from bottle import static_file; return static_file("file.css", root="/static/"). Hook: @app.hook("before_request"); def check_auth(): .... Plugin: app.install(plugin). Run: app.run(host="0.0.0.0", port=8080, debug=True, reloader=True). WSGI: app.run(server="gunicorn"). app.run(server="gevent"). application = app for uWSGI. Default app: from bottle import route, run (module-level singleton). Claude Code generates Bottle REST APIs, auth middleware, CRUD handlers, and WSGI deployment configs.

CLAUDE.md for Bottle

## Bottle Stack
- Version: bottle >= 0.12 | pip install bottle
- App: app = Bottle() | @app.route("/path", method=["GET","POST"])
- Request: request.query.key | request.forms.key | request.json | request.headers
- Response: return {"key": "val"} (auto-JSON) | response.status = 201 | abort(404)
- Hook: @app.hook("before_request") | @app.hook("after_request")
- Run: app.run(host="0.0.0.0", port=8080, server="gunicorn")

Bottle REST API Pipeline

# app/api.py — Bottle routes, JSON helpers, auth, CRUD, hooks, error handling
from __future__ import annotations

import functools
import hashlib
import hmac
import json
import logging
import time
from dataclasses import dataclass, asdict, field
from typing import Any, Callable

from bottle import (
    Bottle,
    HTTPError,
    HTTPResponse,
    abort,
    redirect,
    request,
    response,
    static_file,
)


log = logging.getLogger(__name__)

app = Bottle()


# ─────────────────────────────────────────────────────────────────────────────
# 1. Response helpers
# ─────────────────────────────────────────────────────────────────────────────

def json_response(data: Any, status: int = 200) -> str:
    """
    Return JSON string and set Content-Type to application/json.
    Bottle auto-converts dict returns, but this gives explicit status control.

    Example:
        return json_response({"id": 42, "name": "Widget"}, status=201)
    """
    response.content_type = "application/json"
    response.status       = status
    return json.dumps(data, default=str)


def error_response(message: str, status: int = 400, code: str | None = None) -> str:
    """Return a structured JSON error response."""
    body = {"error": message}
    if code:
        body["code"] = code
    return json_response(body, status=status)


def paginate(items: list, page: int = 1, per_page: int = 20) -> dict:
    """Paginate a list and return metadata dict."""
    total  = len(items)
    offset = (page - 1) * per_page
    return {
        "items":    items[offset: offset + per_page],
        "total":    total,
        "page":     page,
        "per_page": per_page,
        "pages":    (total + per_page - 1) // per_page,
    }


def get_json_body(required: list[str] | None = None) -> dict:
    """
    Parse request JSON body, validating required keys.
    Raises HTTPError(400) on missing keys or invalid JSON.
    """
    try:
        body = request.json
    except Exception:
        raise HTTPError(400, "Invalid JSON")

    if body is None:
        raise HTTPError(400, "JSON body required")

    for key in required or []:
        if key not in body:
            raise HTTPError(422, f"Missing required field: {key}")

    return body


# ─────────────────────────────────────────────────────────────────────────────
# 2. Auth middleware
# ─────────────────────────────────────────────────────────────────────────────

_VALID_TOKENS: set[str] = {"dev-token-123", "api-key-abc"}


def require_auth(fn: Callable) -> Callable:
    """
    Decorator: require Authorization: Bearer <token> header.
    Grants request.environ["auth_token"] to the handler.

    Usage:
        @app.get("/protected")
        @require_auth
        def protected():
            return json_response({"user": "authenticated"})
    """
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        auth = request.headers.get("Authorization", "")
        if not auth.startswith("Bearer "):
            return json_response({"error": "missing token"}, status=401)
        token = auth[len("Bearer "):]
        if token not in _VALID_TOKENS:
            return json_response({"error": "invalid token"}, status=403)
        request.environ["auth_token"] = token
        return fn(*args, **kwargs)
    return wrapper


def require_json(fn: Callable) -> Callable:
    """Decorator: require Content-Type: application/json."""
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        ct = request.content_type or ""
        if "application/json" not in ct:
            return json_response({"error": "Content-Type must be application/json"}, status=415)
        return fn(*args, **kwargs)
    return wrapper


# ─────────────────────────────────────────────────────────────────────────────
# 3. Request lifecycle hooks
# ─────────────────────────────────────────────────────────────────────────────

@app.hook("before_request")
def log_request():
    request.environ["_start_time"] = time.monotonic()
    log.debug("%s %s", request.method, request.path)


@app.hook("after_request")
def add_cors():
    response.headers["Access-Control-Allow-Origin"]  = "*"
    response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type"
    response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
    elapsed = time.monotonic() - request.environ.get("_start_time", time.monotonic())
    response.headers["X-Response-Time"] = f"{elapsed*1000:.1f}ms"


@app.route("/<path:path>", method="OPTIONS")
def handle_options(path: str):
    return json_response({}, status=204)


# ─────────────────────────────────────────────────────────────────────────────
# 4. Error handlers
# ─────────────────────────────────────────────────────────────────────────────

@app.error(400)
def bad_request(e):
    response.content_type = "application/json"
    return json.dumps({"error": str(e.body), "status": 400})


@app.error(401)
def unauthorized(e):
    response.content_type = "application/json"
    return json.dumps({"error": "unauthorized", "status": 401})


@app.error(404)
def not_found(e):
    response.content_type = "application/json"
    return json.dumps({"error": "not found", "status": 404})


@app.error(500)
def server_error(e):
    log.exception("Internal server error")
    response.content_type = "application/json"
    return json.dumps({"error": "internal server error", "status": 500})


# ─────────────────────────────────────────────────────────────────────────────
# 5. CRUD resource
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class Item:
    id:     int
    name:   str
    tags:   list[str] = field(default_factory=list)
    active: bool = True


_items: dict[int, Item] = {}
_next_id = 1


def _new_id() -> int:
    global _next_id
    nid = _next_id
    _next_id += 1
    return nid


@app.get("/health")
def health():
    return json_response({"status": "ok", "items": len(_items)})


@app.get("/items")
def list_items():
    page     = int(request.query.get("page", 1))
    per_page = int(request.query.get("per_page", 20))
    q        = request.query.get("q", "").lower()

    items = list(_items.values())
    if q:
        items = [i for i in items if q in i.name.lower()]

    result = paginate([asdict(i) for i in items], page, per_page)
    return json_response(result)


@app.post("/items")
@require_json
def create_item():
    body = get_json_body(required=["name"])
    item = Item(
        id=_new_id(),
        name=body["name"],
        tags=body.get("tags", []),
        active=body.get("active", True),
    )
    _items[item.id] = item
    return json_response(asdict(item), status=201)


@app.get("/items/<item_id:int>")
def get_item(item_id: int):
    item = _items.get(item_id)
    if not item:
        abort(404, f"Item {item_id} not found")
    return json_response(asdict(item))


@app.put("/items/<item_id:int>")
@require_json
def update_item(item_id: int):
    item = _items.get(item_id)
    if not item:
        abort(404, f"Item {item_id} not found")
    body = request.json or {}
    if "name"   in body: item.name   = body["name"]
    if "tags"   in body: item.tags   = body["tags"]
    if "active" in body: item.active = body["active"]
    return json_response(asdict(item))


@app.delete("/items/<item_id:int>")
def delete_item(item_id: int):
    if item_id not in _items:
        abort(404, f"Item {item_id} not found")
    del _items[item_id]
    return json_response({"deleted": item_id})


@app.get("/items/search")
def search_items():
    q = request.query.get("q", "").strip()
    if not q:
        abort(400, "Search query 'q' required")
    matched = [asdict(i) for i in _items.values() if q.lower() in i.name.lower()]
    return json_response({"results": matched, "count": len(matched)})


@app.get("/protected")
@require_auth
def protected():
    return json_response({"message": "secret data", "token": request.environ["auth_token"]})


# ─────────────────────────────────────────────────────────────────────────────
# 6. Static files and templates
# ─────────────────────────────────────────────────────────────────────────────

@app.get("/static/<filename:path>")
def serve_static(filename: str):
    return static_file(filename, root="./static/")


# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    import urllib.request as req

    # Seed data
    _items[1] = Item(id=1, name="Widget",  tags=["hardware"])
    _items[2] = Item(id=2, name="Gadget",  tags=["electronics"])
    _items[3] = Item(id=3, name="Doohickey", tags=["misc"])
    _next_id  = 4

    from bottle import run as bottle_run
    import threading, time

    def start():
        bottle_run(app, host="127.0.0.1", port=18080, quiet=True)

    t = threading.Thread(target=start, daemon=True)
    t.start()
    time.sleep(0.5)

    base = "http://127.0.0.1:18080"
    print("=== GET /health ===")
    r = req.urlopen(f"{base}/health")
    print(f"  {r.status}: {r.read().decode()}")

    print("\n=== GET /items ===")
    r = req.urlopen(f"{base}/items")
    data = json.loads(r.read())
    print(f"  total={data['total']}, items={[i['name'] for i in data['items']]}")

    print("\n=== POST /items ===")
    payload = json.dumps({"name": "SuperWidget", "tags": ["new"]}).encode()
    r = req.urlopen(req.Request(f"{base}/items", data=payload,
                                headers={"Content-Type": "application/json"},
                                method="POST"))
    print(f"  {r.status}: {r.read().decode()}")

    print("\nBottle app ready. Run with:")
    print("  python api.py  # uses built-in wsgiref server")
    print("  gunicorn api:app -w 4 -b 0.0.0.0:8080")

For the Flask alternative — Flask has a larger ecosystem (Flask-Login, Flask-SQLAlchemy, Flask-Migrate, Flask-WTF), Blueprints for modular apps, and an application factory pattern; Bottle is a single file (no dependencies) making it ideal for embeddable scripts, quick prototypes, and microservices where installing Flask’s full dependency tree is undesirable — use Bottle for truly minimal single-file services, Flask for applications that will grow past a few routes. For the FastAPI alternative — FastAPI provides automatic request validation via Pydantic models, Swagger UI documentation, async request handlers, and OpenAPI schema generation; Bottle is synchronous and has no built-in validation — use FastAPI when you need type-safe APIs with auto-docs, Bottle when you want a zero-dependency WSGI server you can deploy by copying a single .py file. The Claude Skills 360 bundle includes Bottle skill sets covering json_response()/error_response()/paginate() helpers, get_json_body() with required-fields validation, require_auth()/require_json() decorators, before_request/after_request hooks with CORS and timing, error handlers for 400/401/404/500, CRUD routes with GET/POST/PUT/DELETE, search endpoint, OPTIONS preflight handler, static_file() serving, and gunicorn deployment configuration. Start with the free tier to try single-file micro web framework code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free