kinglet


Namekinglet JSON
Version 1.1.0 PyPI version JSON
download
home_pageNone
SummaryA lightweight routing framework for Python Workers
upload_time2025-08-09 07:40:00
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseMIT
keywords cloudflare workers python routing web framework asgi lightweight
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            <div align="center">
  <img src="logo.png" alt="Kinglet Logo" width="200" height="200">
  <h1>Kinglet</h1>
  <p><strong>Lightning-fast Python web framework for Cloudflare Workers</strong></p>
</div>

## Quick Start

Available on PyPi either: run `pip install kinglet` or add to pyproject.toml `dependencies = ["kinglet"]`

If you can't install packages: embed `kinglet/kinglet.py` into your worker/src or project

```python
# Deploy to your ASGI environment
from kinglet import Kinglet

app = Kinglet(root_path="/api")

@app.post("/auth/login")
async def login(request):
    data = await request.json()
    return {"token": "jwt-token", "user": data["email"]}
```

## Why Kinglet?

| Feature | Kinglet | FastAPI | Flask |
|---------|---------|---------|-------|
| **Bundle Size** | 29KB | 7.8MB | 1.9MB |
| **Testing** | No server needed | Requires TestServer | Requires test client |
| **Workers Ready** | ✅ Built-in | ❌ Complex setup | ❌ Not compatible |

*In practical terms FastAPI's load time (especially on cold start) may exceed the worker allownace of cloudflare. Additionally Flask, Bottle and co have different expectations for the tuple that ASGI passes in.*

## Core Features

### **Root Path Support**
Perfect for `/api` behind Cloudflare Pages:

```python
app = Kinglet(root_path="/api")

@app.get("/users")  # Handles /api/users
async def get_users(request):
    return {"users": []}
```

### **Typed Parameters** 
Built-in validation for query and path parameters:

```python
@app.get("/search")
async def search(request):
    limit = request.query_int("limit", 10)        # Returns int or 400 error
    enabled = request.query_bool("enabled", False) # Returns True/False
    tags = request.query_all("tags")              # Returns list of values

@app.get("/users/{user_id}")
async def get_user(request):
    user_id = request.path_param_int("user_id")   # Returns int or 400 error
    uuid = request.path_param_uuid("uuid")        # Validates UUID format
```

### **Authentication Helpers**
Parse Bearer tokens and Basic auth automatically:

```python
@app.get("/protected")
async def protected_route(request):
    token = request.bearer_token()        # Extract JWT from Authorization header
    user, password = request.basic_auth() # Parse Basic authentication
    is_authed = request.is_authenticated() # True if any auth present
    
    if not token:
        return Response.error("Authentication required", 401)
    return {"user": "authenticated"}
```

### **Exception Wrapping & Access Control**
Automatic error handling and endpoint restrictions:

```python
app = Kinglet(debug=True)  # Auto-wraps exceptions with request IDs

@app.get("/admin/debug")
@require_dev()  # 403 in production
@geo_restrict(allowed=["US", "CA"]) 
async def debug_endpoint(request):
    raise ValueError("Auto-wrapped with context")
```

### **Zero-Dependency Testing**
Test without HTTP servers - runs in <1ms:

```python
def test_my_api():
    client = TestClient(app)
    
    status, headers, body = client.request("GET", "/search?limit=5&enabled=true")
    assert status == 200
    
    status, headers, body = client.request("GET", "/protected", headers={
        "Authorization": "Bearer jwt-token-123"
    })
    assert status == 200
```

## Learn More

- **[Quick Examples](examples/)** - Basic API and decorators examples
- **[Testing Guide](docs/TESTING.md)** - Unit & integration testing  
- **[Cloudflare Setup](docs/CLOUDFLARE.md)** - Workers deployment
- **[API Reference](docs/API.md)** - Complete method docs

## Production Ready

- **Request ID tracing** for debugging
- **Typed parameter validation** (int, bool, UUID)
- **Built-in authentication helpers** (Bearer, Basic auth)
- **Automatic exception wrapping** with environment-aware details
- **Access control decorators** (dev-only, geo-restrictions)
- **Configurable CORS** for security
- **Error boundaries** with proper status codes
- **Debug mode** for development
- **Type hints** for better DX
- **Zero-dependency testing** with TestClient

## Contributing

Built for the Cloudflare Workers Python community. PRs welcome for:

- Performance optimizations
- Additional middleware patterns
- Better TypeScript integration
- More testing utilities

---

**Need help?** Check the [docs](docs/) or [open an issue](https://github.com/mitchins/Kinglet/issues).

---

## Full API Example

```python
from kinglet import Kinglet, Response, TestClient

# Create app with root path for /api endpoints
app = Kinglet(root_path="/api", debug=True)

@app.get("/")
async def health_check(request):
    return {"status": "healthy", "request_id": request.request_id}

@app.post("/auth/register") 
async def register(request):
    data = await request.json()
    
    if not data.get("email"):
        return Response.error("Email required", status=400, 
                            request_id=request.request_id)
    
    # Simulate user creation
    return Response.json({
        "user_id": "123",
        "email": data["email"], 
        "created": True
    }, request_id=request.request_id)

@app.get("/users/{user_id}")
async def get_user(request):
    # Typed path parameter with validation
    user_id = request.path_param_int("user_id")  # Returns int or 400 error
    
    # Check authentication
    token = request.bearer_token()
    if not token:
        return Response.error("Authentication required", status=401,
                            request_id=request.request_id)
    
    # Access environment (Cloudflare bindings) 
    db = request.env.DB
    user = await db.prepare("SELECT * FROM users WHERE id = ?").bind(user_id).first()
    
    if not user:
        return Response.error("User not found", status=404,
                            request_id=request.request_id) 
    
    return {"user": user.to_py(), "token": token}

@app.get("/search")
async def search_users(request):
    # Typed query parameters
    page = request.query_int("page", 1)
    limit = request.query_int("limit", 10) 
    active_only = request.query_bool("active", False)
    tags = request.query_all("tags")
    
    return {
        "users": [f"user_{i}" for i in range((page-1)*limit, page*limit)],
        "filters": {"active": active_only, "tags": tags},
        "pagination": {"page": page, "limit": limit}
    }


# Production: Cloudflare Workers entry point
async def on_fetch(request, env):
    return await app(request, env)

# Development: Test without server
if __name__ == "__main__":
    client = TestClient(app)
    
    # Test health check
    status, headers, body = client.request("GET", "/")
    print(f"Health: {status} - {body}")
    
    # Test registration  
    status, headers, body = client.request("POST", "/auth/register", json={
        "email": "test@example.com",
        "password": "secure123"
    })
    print(f"Register: {status} - {body}")
    
    # Test authenticated user lookup
    status, headers, body = client.request("GET", "/users/42", headers={
        "Authorization": "Bearer user-token-123"
    })
    print(f"User: {status} - {body}")
    
    # Test typed query parameters
    status, headers, body = client.request("GET", "/search?page=2&limit=5&active=true&tags=python")
    print(f"Search: {status} - {body}")
    
    # Test error handling
    status, headers, body = client.request("POST", "/auth/register", json={})
    print(f"Error: {status} - {body}")
```

**Output:**
```
Health: 200 - {"status": "healthy", "request_id": "a1b2c3d4"}
Register: 200 - {"user_id": "123", "email": "test@example.com", "created": true, "request_id": "e5f6g7h8"}
User: 200 - {"user": {"id": 42, "email": "test@example.com"}, "token": "user-token-123"}
Search: 200 - {"users": ["user_5", "user_6", "user_7", "user_8", "user_9"], "filters": {"active": true, "tags": ["python"]}, "pagination": {"page": 2, "limit": 5}}
Error: 400 - {"error": "Email required", "status_code": 400, "request_id": "i9j0k1l2"}
```

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "kinglet",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": "cloudflare, workers, python, routing, web, framework, asgi, lightweight",
    "author": null,
    "author_email": "Mitchell Currie <mitchell@mitchins.dev>",
    "download_url": "https://files.pythonhosted.org/packages/ed/22/312ad933bc3ea90c81b0f6171a76a3e38014a280a48f4f51ffed856990aa/kinglet-1.1.0.tar.gz",
    "platform": null,
    "description": "<div align=\"center\">\n  <img src=\"logo.png\" alt=\"Kinglet Logo\" width=\"200\" height=\"200\">\n  <h1>Kinglet</h1>\n  <p><strong>Lightning-fast Python web framework for Cloudflare Workers</strong></p>\n</div>\n\n## Quick Start\n\nAvailable on PyPi either: run `pip install kinglet` or add to pyproject.toml `dependencies = [\"kinglet\"]`\n\nIf you can't install packages: embed `kinglet/kinglet.py` into your worker/src or project\n\n```python\n# Deploy to your ASGI environment\nfrom kinglet import Kinglet\n\napp = Kinglet(root_path=\"/api\")\n\n@app.post(\"/auth/login\")\nasync def login(request):\n    data = await request.json()\n    return {\"token\": \"jwt-token\", \"user\": data[\"email\"]}\n```\n\n## Why Kinglet?\n\n| Feature | Kinglet | FastAPI | Flask |\n|---------|---------|---------|-------|\n| **Bundle Size** | 29KB | 7.8MB | 1.9MB |\n| **Testing** | No server needed | Requires TestServer | Requires test client |\n| **Workers Ready** | \u2705 Built-in | \u274c Complex setup | \u274c Not compatible |\n\n*In practical terms FastAPI's load time (especially on cold start) may exceed the worker allownace of cloudflare. Additionally Flask, Bottle and co have different expectations for the tuple that ASGI passes in.*\n\n## Core Features\n\n### **Root Path Support**\nPerfect for `/api` behind Cloudflare Pages:\n\n```python\napp = Kinglet(root_path=\"/api\")\n\n@app.get(\"/users\")  # Handles /api/users\nasync def get_users(request):\n    return {\"users\": []}\n```\n\n### **Typed Parameters** \nBuilt-in validation for query and path parameters:\n\n```python\n@app.get(\"/search\")\nasync def search(request):\n    limit = request.query_int(\"limit\", 10)        # Returns int or 400 error\n    enabled = request.query_bool(\"enabled\", False) # Returns True/False\n    tags = request.query_all(\"tags\")              # Returns list of values\n\n@app.get(\"/users/{user_id}\")\nasync def get_user(request):\n    user_id = request.path_param_int(\"user_id\")   # Returns int or 400 error\n    uuid = request.path_param_uuid(\"uuid\")        # Validates UUID format\n```\n\n### **Authentication Helpers**\nParse Bearer tokens and Basic auth automatically:\n\n```python\n@app.get(\"/protected\")\nasync def protected_route(request):\n    token = request.bearer_token()        # Extract JWT from Authorization header\n    user, password = request.basic_auth() # Parse Basic authentication\n    is_authed = request.is_authenticated() # True if any auth present\n    \n    if not token:\n        return Response.error(\"Authentication required\", 401)\n    return {\"user\": \"authenticated\"}\n```\n\n### **Exception Wrapping & Access Control**\nAutomatic error handling and endpoint restrictions:\n\n```python\napp = Kinglet(debug=True)  # Auto-wraps exceptions with request IDs\n\n@app.get(\"/admin/debug\")\n@require_dev()  # 403 in production\n@geo_restrict(allowed=[\"US\", \"CA\"]) \nasync def debug_endpoint(request):\n    raise ValueError(\"Auto-wrapped with context\")\n```\n\n### **Zero-Dependency Testing**\nTest without HTTP servers - runs in <1ms:\n\n```python\ndef test_my_api():\n    client = TestClient(app)\n    \n    status, headers, body = client.request(\"GET\", \"/search?limit=5&enabled=true\")\n    assert status == 200\n    \n    status, headers, body = client.request(\"GET\", \"/protected\", headers={\n        \"Authorization\": \"Bearer jwt-token-123\"\n    })\n    assert status == 200\n```\n\n## Learn More\n\n- **[Quick Examples](examples/)** - Basic API and decorators examples\n- **[Testing Guide](docs/TESTING.md)** - Unit & integration testing  \n- **[Cloudflare Setup](docs/CLOUDFLARE.md)** - Workers deployment\n- **[API Reference](docs/API.md)** - Complete method docs\n\n## Production Ready\n\n- **Request ID tracing** for debugging\n- **Typed parameter validation** (int, bool, UUID)\n- **Built-in authentication helpers** (Bearer, Basic auth)\n- **Automatic exception wrapping** with environment-aware details\n- **Access control decorators** (dev-only, geo-restrictions)\n- **Configurable CORS** for security\n- **Error boundaries** with proper status codes\n- **Debug mode** for development\n- **Type hints** for better DX\n- **Zero-dependency testing** with TestClient\n\n## Contributing\n\nBuilt for the Cloudflare Workers Python community. PRs welcome for:\n\n- Performance optimizations\n- Additional middleware patterns\n- Better TypeScript integration\n- More testing utilities\n\n---\n\n**Need help?** Check the [docs](docs/) or [open an issue](https://github.com/mitchins/Kinglet/issues).\n\n---\n\n## Full API Example\n\n```python\nfrom kinglet import Kinglet, Response, TestClient\n\n# Create app with root path for /api endpoints\napp = Kinglet(root_path=\"/api\", debug=True)\n\n@app.get(\"/\")\nasync def health_check(request):\n    return {\"status\": \"healthy\", \"request_id\": request.request_id}\n\n@app.post(\"/auth/register\") \nasync def register(request):\n    data = await request.json()\n    \n    if not data.get(\"email\"):\n        return Response.error(\"Email required\", status=400, \n                            request_id=request.request_id)\n    \n    # Simulate user creation\n    return Response.json({\n        \"user_id\": \"123\",\n        \"email\": data[\"email\"], \n        \"created\": True\n    }, request_id=request.request_id)\n\n@app.get(\"/users/{user_id}\")\nasync def get_user(request):\n    # Typed path parameter with validation\n    user_id = request.path_param_int(\"user_id\")  # Returns int or 400 error\n    \n    # Check authentication\n    token = request.bearer_token()\n    if not token:\n        return Response.error(\"Authentication required\", status=401,\n                            request_id=request.request_id)\n    \n    # Access environment (Cloudflare bindings) \n    db = request.env.DB\n    user = await db.prepare(\"SELECT * FROM users WHERE id = ?\").bind(user_id).first()\n    \n    if not user:\n        return Response.error(\"User not found\", status=404,\n                            request_id=request.request_id) \n    \n    return {\"user\": user.to_py(), \"token\": token}\n\n@app.get(\"/search\")\nasync def search_users(request):\n    # Typed query parameters\n    page = request.query_int(\"page\", 1)\n    limit = request.query_int(\"limit\", 10) \n    active_only = request.query_bool(\"active\", False)\n    tags = request.query_all(\"tags\")\n    \n    return {\n        \"users\": [f\"user_{i}\" for i in range((page-1)*limit, page*limit)],\n        \"filters\": {\"active\": active_only, \"tags\": tags},\n        \"pagination\": {\"page\": page, \"limit\": limit}\n    }\n\n\n# Production: Cloudflare Workers entry point\nasync def on_fetch(request, env):\n    return await app(request, env)\n\n# Development: Test without server\nif __name__ == \"__main__\":\n    client = TestClient(app)\n    \n    # Test health check\n    status, headers, body = client.request(\"GET\", \"/\")\n    print(f\"Health: {status} - {body}\")\n    \n    # Test registration  \n    status, headers, body = client.request(\"POST\", \"/auth/register\", json={\n        \"email\": \"test@example.com\",\n        \"password\": \"secure123\"\n    })\n    print(f\"Register: {status} - {body}\")\n    \n    # Test authenticated user lookup\n    status, headers, body = client.request(\"GET\", \"/users/42\", headers={\n        \"Authorization\": \"Bearer user-token-123\"\n    })\n    print(f\"User: {status} - {body}\")\n    \n    # Test typed query parameters\n    status, headers, body = client.request(\"GET\", \"/search?page=2&limit=5&active=true&tags=python\")\n    print(f\"Search: {status} - {body}\")\n    \n    # Test error handling\n    status, headers, body = client.request(\"POST\", \"/auth/register\", json={})\n    print(f\"Error: {status} - {body}\")\n```\n\n**Output:**\n```\nHealth: 200 - {\"status\": \"healthy\", \"request_id\": \"a1b2c3d4\"}\nRegister: 200 - {\"user_id\": \"123\", \"email\": \"test@example.com\", \"created\": true, \"request_id\": \"e5f6g7h8\"}\nUser: 200 - {\"user\": {\"id\": 42, \"email\": \"test@example.com\"}, \"token\": \"user-token-123\"}\nSearch: 200 - {\"users\": [\"user_5\", \"user_6\", \"user_7\", \"user_8\", \"user_9\"], \"filters\": {\"active\": true, \"tags\": [\"python\"]}, \"pagination\": {\"page\": 2, \"limit\": 5}}\nError: 400 - {\"error\": \"Email required\", \"status_code\": 400, \"request_id\": \"i9j0k1l2\"}\n```\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "A lightweight routing framework for Python Workers",
    "version": "1.1.0",
    "project_urls": {
        "Bug Tracker": "https://github.com/mitchins/Kinglet/issues",
        "Homepage": "https://github.com/mitchins/Kinglet",
        "Repository": "https://github.com/mitchins/Kinglet"
    },
    "split_keywords": [
        "cloudflare",
        " workers",
        " python",
        " routing",
        " web",
        " framework",
        " asgi",
        " lightweight"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "3dae8632f53882f23549dcc0afde3b02e3a171d0cdf0be1ec510733992c7cd61",
                "md5": "809a56553933e8531c261cf8f5a5dc37",
                "sha256": "64b6cc5bc5e61312ad3bf76ae47999eb7743087dbb88dca4f5e0fb4aef44acdc"
            },
            "downloads": -1,
            "filename": "kinglet-1.1.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "809a56553933e8531c261cf8f5a5dc37",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 13821,
            "upload_time": "2025-08-09T07:39:59",
            "upload_time_iso_8601": "2025-08-09T07:39:59.818783Z",
            "url": "https://files.pythonhosted.org/packages/3d/ae/8632f53882f23549dcc0afde3b02e3a171d0cdf0be1ec510733992c7cd61/kinglet-1.1.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "ed22312ad933bc3ea90c81b0f6171a76a3e38014a280a48f4f51ffed856990aa",
                "md5": "d91dd5c96eeacba330bb952e91315054",
                "sha256": "f76f5cb6252b0d25d496e7f7cccb13175c25d7917a1ca6f952ac598428aa64c9"
            },
            "downloads": -1,
            "filename": "kinglet-1.1.0.tar.gz",
            "has_sig": false,
            "md5_digest": "d91dd5c96eeacba330bb952e91315054",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 24076,
            "upload_time": "2025-08-09T07:40:00",
            "upload_time_iso_8601": "2025-08-09T07:40:00.981126Z",
            "url": "https://files.pythonhosted.org/packages/ed/22/312ad933bc3ea90c81b0f6171a76a3e38014a280a48f4f51ffed856990aa/kinglet-1.1.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-08-09 07:40:00",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "mitchins",
    "github_project": "Kinglet",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "kinglet"
}
        
Elapsed time: 0.55242s