A high-performance web framework for Python, powered by Rust and Axum.
- âś… High-performance Rust backend with Axum web framework
- âś… Simple and intuitive Python API
- âś… Support for all standard HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
- âś… Convenient method-specific decorators (
@app.get(),@app.post(), etc.) - âś… Dynamic route parameters (e.g.,
/user/<username>) - âś… Request body parsing for POST, PUT, PATCH, and DELETE
- âś… Async/await support
- âś… JSON-formatted request logging
- âś… OpenTelemetry support for metrics, tracing, and logging
- âś… Middleware support for request/response processing
- âś… Cookie support (get/set cookies in requests and responses)
- âś… Authentication token support (Bearer token helper)
- âś… Static file serving via decorators
- âś… Reverse proxy support via decorators
- âś… OpenAPI/Swagger JSON endpoint
- âś… File upload support with streaming (memory-efficient)
Once released, you can install Rupy from PyPI:
pip install rupy-apiOr add it to your pyproject.toml:
[project]
dependencies = [
"rupy-api>=0.1.0"
]To add Rupy as a dependency to your project using the GitHub repository, add the following to your pyproject.toml:
[project]
dependencies = [
"rupy @ git+https://github.com/manoelhc/rupy.git"
]Or for a specific branch, tag, or commit:
[project]
dependencies = [
# Using a specific branch
"rupy @ git+https://github.com/manoelhc/rupy.git@main",
# Using a specific tag
"rupy @ git+https://github.com/manoelhc/rupy.git@v0.1.0",
# Using a specific commit
"rupy @ git+https://github.com/manoelhc/rupy.git@abc123",
]Then install the dependencies:
pip install .- Python 3.8+
- Rust 1.56+
- maturin
- Install maturin:
pip install maturin- Build the project:
maturin build --release- Install the wheel:
pip install target/wheels/rupy-*.whlOr build and install in development mode:
maturin developfrom rupy import Rupy, Request, Response
app = Rupy()
@app.route("/", methods=["GET"])
def index(request: Request) -> Response:
return Response("Hello, World!")
@app.route("/user/<username>", methods=["GET"])
def get_user(request: Request, username: str) -> Response:
return Response(f"User: {username}")
@app.route("/echo", methods=["POST"])
def echo(request: Request) -> Response:
return Response(f"Echo: {request.body}")
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8000)Rupy supports all standard HTTP methods:
- GET: Retrieve resources
- POST: Create new resources or submit data
- PUT: Update/replace resources
- PATCH: Partially update resources
- DELETE: Remove resources
- HEAD: Retrieve headers only
- OPTIONS: Get supported methods for a resource
For convenience, Rupy provides method-specific decorators that make your code more readable:
from rupy import Rupy, Request, Response
app = Rupy()
# Instead of @app.route("/items", methods=["GET"])
@app.get("/items")
def list_items(request: Request) -> Response:
return Response("List of items")
# Instead of @app.route("/items", methods=["POST"])
@app.post("/items")
def create_item(request: Request) -> Response:
return Response(f"Created: {request.body}")
# Available decorators for all HTTP methods:
@app.put("/items/<item_id>")
def update_item(request: Request, item_id: str) -> Response:
return Response(f"Updated item {item_id}: {request.body}")
@app.patch("/items/<item_id>")
def patch_item(request: Request, item_id: str) -> Response:
return Response(f"Patched item {item_id}: {request.body}")
@app.delete("/items/<item_id>")
def delete_item(request: Request, item_id: str) -> Response:
return Response(f"Deleted item {item_id}")
@app.head("/items")
def head_items(request: Request) -> Response:
return Response("Headers only")
@app.options("/items")
def options_items(request: Request) -> Response:
return Response("OPTIONS response")
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8000)Available method-specific decorators:
@app.get(path)- for GET requests@app.post(path)- for POST requests@app.put(path)- for PUT requests@app.patch(path)- for PATCH requests@app.delete(path)- for DELETE requests@app.head(path)- for HEAD requests@app.options(path)- for OPTIONS requests
You can still use @app.route(path, methods=[...]) when you need to handle multiple methods with the same handler.
You can define dynamic segments in your routes using angle brackets:
@app.route("/user/<username>/post/<post_id>", methods=["GET"])
def get_user_post(request: Request, username: str, post_id: str) -> Response:
return Response(f"Post {post_id} by {username}")Rupy supports middleware functions that execute before route handlers. Middlewares can:
- Inspect and modify requests
- Return early responses (e.g., for authentication)
- Execute in registration order
- Block or allow requests to proceed
from rupy import Rupy, Request, Response
app = Rupy()
@app.middleware
def logging_middleware(request: Request):
print(f"Processing {request.method} {request.path}")
# Return request to continue to next middleware/handler
return request
@app.middleware
def auth_middleware(request: Request):
# Check authentication and block if needed
if request.path.startswith("/admin") and not is_authenticated(request):
return Response("Unauthorized", status=401)
return request
@app.route("/", methods=["GET"])
def index(request: Request) -> Response:
return Response("Hello, World!")@app.middleware
def cors_middleware(request: Request):
print(f"[CORS] Processing {request.method} {request.path}")
# Handle preflight OPTIONS requests
if request.method == "OPTIONS":
return Response("", status=204)
# Continue to next middleware or route handler
return request@app.middleware
def jwt_auth_middleware(request: Request):
# Skip auth for public routes
if request.path in ["/", "/login", "/public"]:
return request
# Check for protected routes
if request.path.startswith("/protected"):
# In real implementation, validate JWT token from headers
return Response(
'{"error": "Unauthorized - Invalid or missing JWT token"}',
status=401
)
return requestFor complete production-ready examples, see:
examples/jwt_middleware.py- JWT authentication with PyJWT libraryexamples/cors_middleware.py- CORS with configurable origins and credentialsexamples/geo_blocking_middleware.py- IP-based geographical access controlexamples/rate_limiting_middleware.py- Rate limiting by IP and User-Agentexamples/combined_middlewares.py- Multiple middlewares working togetherexamples/MIDDLEWARE_README.md- Comprehensive middleware documentation
All middleware examples feature:
- Production-ready implementations with real libraries
- Security best practices and error handling
- Detailed documentation and usage examples
- Testing commands and load testing guidance
Rupy provides built-in support for working with cookies and Bearer authentication tokens.
from rupy import Rupy, Request, Response
app = Rupy()
@app.route("/login", methods=["POST"])
def login(request: Request) -> Response:
resp = Response('{"message": "Login successful"}')
# Set a cookie with options
resp.set_cookie(
"session_id",
"abc123",
max_age=3600, # Expires in 1 hour
http_only=True, # Not accessible via JavaScript
secure=True, # Only sent over HTTPS
same_site="Lax" # CSRF protection
)
return resp
@app.route("/profile", methods=["GET"])
def profile(request: Request) -> Response:
# Read a cookie
session_id = request.get_cookie("session_id")
if not session_id:
return Response("Not logged in", status=401)
return Response(f"Session: {session_id}")
@app.route("/logout", methods=["POST"])
def logout(request: Request) -> Response:
resp = Response('{"message": "Logged out"}')
# Delete a cookie
resp.delete_cookie("session_id")
return resp@app.route("/protected", methods=["GET"])
def protected(request: Request) -> Response:
# Get Bearer token from Authorization header
token = request.auth_token
if not token:
return Response("Unauthorized", status=401)
# Validate token (implement your own validation logic)
if token == "valid-token":
return Response("Access granted")
else:
return Response("Invalid token", status=401)
@app.middleware
def auth_middleware(request: Request):
"""Add authentication token in middleware"""
if request.path.startswith("/internal"):
request.set_auth_token("internal-service-token")
return requestFor complete examples, see:
examples/cookies_auth_example.py- Cookie and auth token handling
Handle file uploads efficiently with the @app.upload() decorator. Files are streamed directly to disk without being loaded into memory, making it suitable for large files.
from rupy import Rupy, Request, Response, UploadFile
from typing import List
app = Rupy()
# Basic file upload
@app.upload("/upload")
def handle_upload(request: Request, files: List[UploadFile]) -> Response:
for file in files:
print(f"Uploaded: {file.filename}")
print(f"Size: {file.size} bytes")
print(f"MIME type: {file.content_type}")
print(f"Saved at: {file.path}")
return Response("Files uploaded successfully")
# Upload with MIME type filtering
@app.upload("/upload-images", accepted_mime_types=["image/*"])
def upload_images(request: Request, files: List[UploadFile]) -> Response:
return Response(f"Uploaded {len(files)} images")
# Upload with size limit (5MB)
@app.upload("/upload-limited", max_size=5*1024*1024)
def upload_limited(request: Request, files: List[UploadFile]) -> Response:
return Response("Upload successful")
# Upload with all options
@app.upload(
"/upload-docs",
accepted_mime_types=["application/pdf", "application/msword"],
max_size=10*1024*1024, # 10MB
upload_dir="/var/uploads"
)
def upload_docs(request: Request, files: List[UploadFile]) -> Response:
return Response("Documents uploaded")Upload features:
- Streaming uploads: Files are written directly to disk to prevent memory overflow
- MIME type filtering: Accept only specific file types (supports wildcards like
image/*) - Size limits: Set maximum file size per upload
- Custom upload directory: Specify where files should be stored (default:
/tmp) - Multiple files: Handle multiple file uploads in a single request
- UploadFile attributes:
filename: Original filenamesize: File size in bytescontent_type: MIME typepath: Temporary file path on disk
For a complete example, see:
examples/upload_example.py- File upload with web interface
Serve static files from a directory using the @app.static() decorator.
from rupy import Rupy, Request, Response
app = Rupy()
# Serve files from ./public directory at /static path
@app.static("/static", "./public")
def static_files():
pass
# Now files in ./public are accessible at /static/<filename>
# Example: ./public/style.css -> http://localhost:8000/static/style.cssThe static file server includes:
- Automatic content-type detection
- Directory traversal protection
- Support for all common file types
For a complete example, see:
examples/static_files_example.py- Static file serving
Proxy requests to another backend service using the @app.proxy() decorator.
from rupy import Rupy, Request, Response
app = Rupy()
# Proxy all /api/* requests to backend service
@app.proxy("/api", "http://backend:8080")
def api_proxy():
pass
# Now requests to /api/* are forwarded to http://backend:8080/*
# Example: /api/users -> http://backend:8080/usersThe reverse proxy:
- Forwards all HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Preserves request headers and body
- Returns response headers and body from the backend
For a complete example, see:
examples/reverse_proxy_example.py- Reverse proxy with backend
Enable OpenAPI documentation for your API.
from rupy import Rupy, Request, Response
app = Rupy()
# Enable OpenAPI endpoint
app.enable_openapi(
path="/openapi.json",
title="My API",
version="1.0.0",
description="API documentation"
)
@app.route("/users", methods=["GET"])
def list_users(request: Request) -> Response:
"""List all users - this docstring can be used for API docs"""
return Response('[{"id": 1, "name": "Alice"}]')
# Access the OpenAPI spec at http://localhost:8000/openapi.jsonTo disable the OpenAPI endpoint:
app.disable_openapi()For a complete example, see:
examples/openapi_example.py- OpenAPI documentation
Run the example:
python example.pyTest with curl:
# GET request
curl http://127.0.0.1:8000/
# GET with parameter
curl http://127.0.0.1:8000/user/alice
# POST request
curl -X POST -d '{"name": "test"}' http://127.0.0.1:8000/echo
# PUT request
curl -X PUT -d '{"name": "updated"}' http://127.0.0.1:8000/items/1
# PATCH request
curl -X PATCH -d '{"status": "active"}' http://127.0.0.1:8000/items/1
# DELETE request
curl -X DELETE http://127.0.0.1:8000/items/1Rupy includes built-in support for OpenTelemetry, providing comprehensive observability through metrics, tracing, and logging.
You can enable OpenTelemetry in two ways:
from rupy import Rupy, Request, Response
app = Rupy()
# Enable telemetry with optional endpoint and service name
app.enable_telemetry(
endpoint="http://localhost:4317", # Optional: OTLP gRPC endpoint
service_name="my-service" # Optional: Service name for traces
)
@app.route("/", methods=["GET"])
def index(request: Request) -> Response:
return Response("Hello, World!")
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8000)Set these environment variables before running your application:
# Enable OpenTelemetry
export OTEL_ENABLED=true
# Set the service name (default: "rupy")
export OTEL_SERVICE_NAME=my-service
# Set the OTLP endpoint (optional)
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# Set the log level (optional)
export RUST_LOG=info
# Run your application
python app.pyRupy provides several methods to control OpenTelemetry:
app = Rupy()
# Enable telemetry
app.enable_telemetry(endpoint="http://localhost:4317", service_name="my-service")
# Disable telemetry
app.disable_telemetry()
# Check if telemetry is enabled
is_enabled = app.is_telemetry_enabled()
# Set service name
app.set_service_name("my-new-service")
# Set OTLP endpoint
app.set_telemetry_endpoint("http://localhost:4317")Rupy automatically collects the following metrics:
-
http.server.requests: Counter for total number of HTTP requests- Labels:
http.method,http.route,http.status_code
- Labels:
-
http.server.duration: Histogram for HTTP request duration in seconds- Labels:
http.method,http.route,http.status_code
- Labels:
Each HTTP request creates a span with the following attributes:
http.method: The HTTP method (GET, POST, etc.)http.route: The matched route patternhttp.scheme: The protocol scheme (http/https)
Spans are nested for handler execution, allowing you to trace the complete request lifecycle.
All logs are emitted in JSON format and include:
- Timestamp
- Log level
- Message
- Request details (method, path, status)
- Handler execution information
Rupy's OpenTelemetry implementation works with any OTLP-compatible backend:
- Jaeger: For distributed tracing
- Prometheus: For metrics collection
- Grafana: For visualization
- OpenTelemetry Collector: For data processing and export
- Datadog, New Relic, Honeycomb: Commercial observability platforms
Example with OpenTelemetry Collector:
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
exporters:
prometheus:
endpoint: "0.0.0.0:8889"
jaeger:
endpoint: "jaeger:14250"
service:
pipelines:
traces:
receivers: [otlp]
exporters: [jaeger]
metrics:
receivers: [otlp]
exporters: [prometheus]Run the collector:
docker run -d \
-v $(pwd)/otel-collector-config.yaml:/etc/otel-collector-config.yaml \
-p 4317:4317 \
-p 8889:8889 \
otel/opentelemetry-collector:latest \
--config=/etc/otel-collector-config.yamlThen configure Rupy to send data to it:
app.enable_telemetry(endpoint="http://localhost:4317", service_name="my-service")| Variable | Description | Default |
|---|---|---|
OTEL_ENABLED |
Enable/disable OpenTelemetry | false |
OTEL_SERVICE_NAME |
Service name for telemetry | rupy |
OTEL_EXPORTER_OTLP_ENDPOINT |
OTLP gRPC endpoint | None |
RUST_LOG |
Log level (trace, debug, info, warn, error) | info |
- Rust Backend: Uses Axum web framework for high-performance HTTP handling
- Python Bindings: PyO3 provides seamless Python-Rust interoperability
- Async Runtime: Tokio powers the asynchronous server
- Observability: OpenTelemetry integration for metrics, tracing, and logging
MIT License - see LICENSE file for details