Lightweight, zero-dependency shape-based payload filtering and sanitization for Python
Project description
payloadguard
Python version of the popular Node.js payload-guard package Part of the Professional Python Backend Toolkit
🛡️ Lightweight, zero-dependency shape-based payload filtering and sanitization for Python
🛡️ Workflow
Request → Gatekeeper → Shape Check → Redact & Clean → Secure Response
↓
Strict Error / Fail Safe
✨ Features
- Shape-based filtering — Define what you want, auto-remove everything else
- Sensitive field protection —
password,token,secretautomatically removed - Zero dependencies — Pure Python, no external packages required
- Framework support — FastAPI, Flask, Django middleware included
- Pydantic integration — Seamless integration with Pydantic models
- Type-safe — Full type hints for better IDE support
- Blazing fast — Optimized for production performance
- Never crashes — Graceful failure mode, production-safe
📦 Installation
Basic Installation
pip install payloadguard
With Framework Support
# FastAPI
pip install "payloadguard[fastapi]"
# Flask
pip install "payloadguard[flask]"
# Django
pip install "payloadguard[django]"
# Pydantic
pip install "payloadguard[pydantic]"
# All extras
pip install "payloadguard[all]"
🚀 Quick Start
Basic Usage
from payloadguard import guard
# Define a shape
user_shape = guard.shape({
"id": "number",
"name": "string",
"email": "string",
})
# Filter data
raw_data = {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"password": "secret123", # ❌ Will be removed
"internal_notes": "VIP", # ❌ Will be removed
}
safe_data = user_shape(raw_data)
# Result: {"id": 1, "name": "John Doe", "email": "john@example.com"}
Advanced Validation
from payloadguard import guard
user_shape = guard.shape({
"email": guard.string().email().to_lower().trim(),
"age": guard.number().min(18).max(100).default(18),
"username": guard.string().min(3).max(20),
"role": guard.string().validate(lambda v: v in ["admin", "user"]).default("user"),
})
data = user_shape({
"email": " JOHN@Example.com ",
"age": 25,
"username": "john_doe",
})
# Result: {"email": "john@example.com", "age": 25, "username": "john_doe"}
🎯 Nested Object Validation (v1.0+)
Define shapes for deeply nested objects:
from payloadguard import guard
user_shape = guard.shape({
"id": "number",
"profile": {
"name": "string",
"email": guard.string().email(),
"age": guard.number().min(18),
"address": {
"street": "string",
"city": "string",
"zip_code": guard.string().regex(r"^\d{5}(-\d{4})?$"),
},
},
"posts": guard.array({
"id": "number",
"title": "string",
"tags": guard.array("string"),
}),
})
data = user_shape({
"id": 1,
"profile": {
"name": "John",
"email": "john@example.com",
"age": 30,
"address": {
"street": "123 Main St",
"city": "NYC",
"zip_code": "10001",
},
},
"posts": [
{"id": 1, "title": "Hello", "tags": ["intro"]},
],
})
🔒 Custom Error Messages (v1.0+)
from payloadguard import guard
user_shape = guard.shape({
"email": guard
.string()
.email()
.error("Please provide a valid email address"),
"username": guard
.string()
.min(5)
.error(lambda value, field: f"{field} must be at least 5 characters"),
"age": guard
.number()
.min(18)
.error_codes({"min": "AGE_TOO_YOUNG", "max": "AGE_TOO_OLD"}),
})
Error Collection
from payloadguard import compile_shape, FieldConfig
errors = []
validator = compile_shape(
{
"email": FieldConfig(type="string", email=True),
"age": FieldConfig(type="number", min=18),
},
collect_errors=True,
errors=errors,
)
validator({"email": "invalid", "age": 15})
print(errors)
# [
# ValidationError(field="email", message="...", code="email"),
# ValidationError(field="age", message="...", code="min"),
# ]
🌐 Framework Integration
FastAPI
from fastapi import FastAPI
from payloadguard.middleware.fastapi import PayloadGuardMiddleware
app = FastAPI()
request_shape = {
"name": "string",
"email": guard.string().email(),
}
app.add_middleware(
PayloadGuardMiddleware,
request_shape=request_shape,
dev_mode=True,
)
@app.post("/users")
async def create_user(request: dict):
# Request body is already sanitized
return request
Flask
from flask import Flask, request, jsonify
from payloadguard.middleware.flask import PayloadGuardMiddleware, route_guard
app = Flask(__name__)
PayloadGuardMiddleware(app, request_shape={
"name": "string",
"email": "string",
})
@app.route("/users", methods=["POST"])
@route_guard({
"name": "string",
"email": guard.string().email(),
})
def create_user():
from payloadguard.middleware.flask import get_sanitized_body
data = get_sanitized_body()
return jsonify(data)
Django
# settings.py
MIDDLEWARE = [
# ...
"payloadguard.middleware.django.PayloadGuardMiddleware",
]
PAYLOAD_GUARD_CONFIG = {
"request_shape": {
"name": "string",
"email": "string",
},
"dev_mode": True,
}
# views.py
from django.http import JsonResponse
from payloadguard.middleware.django import route_guard, get_sanitized_body
@route_guard({
"name": "string",
"email": "string",
})
def create_user(request):
data = get_sanitized_body(request)
return JsonResponse(data)
🔷 Pydantic Integration
from pydantic import BaseModel
from payloadguard.integrations.pydantic import GuardedModel, guard_model
# Option 1: Using GuardedModel base class
class UserCreate(GuardedModel):
name: str
email: str
age: int
class Config:
guard_shape = {
"name": "string",
"email": "string",
"age": "number",
}
# Create from raw data
user = UserCreate.from_raw({
"name": "John",
"email": "john@example.com",
"age": 30,
"extra": "field", # Will be removed
})
# Option 2: Using decorator
@guard_model({
"name": "string",
"email": "string",
})
class UserUpdate(BaseModel):
name: str
email: str
user = UserUpdate.from_raw({"name": "Jane", "email": "jane@example.com"})
📖 API Reference
Main API
from payloadguard import guard, Guard, build_shape, compile_shape
# Default guard instance
guard = Guard()
# Create shape
shape = guard.shape({...})
# Create custom guard with config
custom_guard = Guard()
custom_guard.config(
dev_mode=True,
sensitive_fields=["custom_secret"],
)
# String builder
guard.string()
.min(length)
.max(length)
.email()
.regex(pattern)
.trim()
.to_lower()
.to_upper()
.required()
.default(value)
.transform(fn)
.validate(fn)
.error(message)
.error_codes(codes)
# Number builder
guard.number()
.min(value)
.max(value)
.integer()
.positive()
.required()
.default(value)
.transform(fn)
.validate(fn)
.error(message)
.error_codes(codes)
Configuration Options
guard.config(
sensitive_fields=["custom_field"], # Additional sensitive fields
dev_mode=True, # Enable dev warnings
strict=True, # Strict type validation
max_array_length=1000, # Max array items to process
collect_errors=True, # Collect validation errors
fail_open=True, # Return original on error
)
⚡ Performance
| Scenario | ops/sec | avg (ms) |
|---|---|---|
| Small payload (5 fields) | 125,000+ | 0.008ms |
| Medium payload (50 items) | 18,000+ | 0.055ms |
| Large payload (1000 items) | 3,500+ | 0.285ms |
🛡️ Security Features
- Automatic sensitive field removal — password, token, secret, etc.
- Circular reference protection — No infinite loops
- Prototype pollution prevention — Safe key handling
- Graceful error handling — Never crashes in production
📝 Complete Example
E-commerce Order Validation
from payloadguard import guard
order_shape = guard.shape({
"order_id": "string",
"customer": {
"id": "number",
"name": "string",
"email": guard.string().email(),
"shipping_address": {
"street": "string",
"city": "string",
"zip_code": guard.string().regex(r"^\d{5}$"),
"country": "string",
},
},
"items": guard.array({
"product_id": "string",
"quantity": guard.number().min(1).max(100),
"price": guard.number().min(0),
}),
"payment": {
"method": "string",
"card_last4": guard.string().regex(r"^\d{4}$"),
# cvv, token automatically removed
},
})
order = order_shape({
"order_id": "ORD-123",
"customer": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"shipping_address": {
"street": "123 Main St",
"city": "New York",
"zip_code": "10001",
"country": "USA",
},
},
"items": [
{"product_id": "PROD-1", "quantity": 2, "price": 29.99},
],
"payment": {
"method": "credit_card",
"card_last4": "1234",
"cvv": "123", # Removed automatically
},
})
🧪 Testing
# Install dev dependencies
pip install "payloadguard[dev]"
# Run tests
pytest
# Run with coverage
pytest --cov=payloadguard --cov-report=html
# Run specific test file
pytest tests/test_core.py -v
📄 License
MIT
🤝 Contributing
Contributions welcome! Please read CONTRIBUTING.md first.
🔮 Roadmap
- Async validation support
- GraphQL integration
- OpenAPI schema generation
- Custom validation decorators
- Redis-based rate limiting integration
Made with ❤️ by @sannuk79
Part of the Professional Backend Toolkit
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 payloadguard-1.0.0.tar.gz.
File metadata
- Download URL: payloadguard-1.0.0.tar.gz
- Upload date:
- Size: 27.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3d80bcb76f205a8c9ca9e0850edf69c6a90093eaec0f396d4501e9efdeb32803
|
|
| MD5 |
d3f5b14afeafd0356749947f664bcd00
|
|
| BLAKE2b-256 |
06f51cc4797a9c578f53796e741bca9473c808f3cdd1f1c9f626489cea75af44
|
File details
Details for the file payloadguard-1.0.0-py3-none-any.whl.
File metadata
- Download URL: payloadguard-1.0.0-py3-none-any.whl
- Upload date:
- Size: 26.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bda37f243a9b5dd6f81fd9fe6cee1c9a1179ddf77eeadea1f9c3c5a48dabf2ae
|
|
| MD5 |
7c8b2de66333e85c2205b60e869f9e64
|
|
| BLAKE2b-256 |
f2f756b6ca41e173091130bd9ee3bcc8513cb3035e985012000b88f68bbc1aab
|