Vedrakit
Vedrakit is a small, dependency-light Python web framework for building HTTP
APIs without hiding the runtime behind a large stack. It started as a
FastAPI-like prototype and is now organized as a reusable package with
production-oriented defaults, isolated App instances, automated tests, and
optional integrations.
Documentation: This README is the product overview and quick-start guide. For the complete public API, signatures, behavior tables, and integration examples, read docs/API.md.
The framework keeps the original prototype's feature surface:
- Decorator-based routing:
get,post,put,delete, androute - Dynamic path parameters, query parameters, JSON bodies, and type coercion
BaseModelrequest/response validation- SQLite models and migrations
- Password hashing, JWT authentication, and role-based access control
- Middleware, exception handlers, dependency injection, and background tasks
- Redis-backed cache/rate limiting with a safe in-memory fallback
- Static files with path traversal protection
- OpenAPI JSON at
/docs - Health and readiness endpoints
- Prometheus-compatible metrics at
/metrics - Optional WebSocket, GraphQL, and gRPC support
Requirements
- Python 3.10 or newer
- No runtime dependency is required for the core HTTP, validation, SQLite, authentication, queue, docs, or metrics features
Optional integrations can be installed separately:
pip install -e ".[redis]"
pip install -e ".[graphql]"
pip install -e ".[websocket]"
pip install -e ".[grpc]"
pip install -e ".[postgresql]"
# or everything:
pip install -e ".[all]"
For local development and the included tests:
pip install -r requirements-dev.txt
python -m unittest discover -s tests -v
Quick start
from vedrakit import App, BaseModel, Query
class UserCreate(BaseModel):
username: str
age: int
app = App()
@app.route("/users", ["POST"])
def create_user(user: UserCreate):
return {"username": user.username, "age": user.age}
@app.route("/users", ["GET"])
def list_users(limit: int = Query(default=20)):
return {"items": [], "limit": limit}
if __name__ == "__main__":
app.run(port=8080)
Run the complete example:
python -m examples.basic_app
Then visit:
GET /health— livenessGET /ready— database readinessGET /docs— generated OpenAPI 3.0.3 documentGET /metrics— Prometheus-compatible metrics
Application objects and decorators
App is recommended for production applications and tests because each
instance has its own routes and middleware:
from vedrakit import App
app = App(static_dir="static")
@app.route("/users/{user_id}", ["GET"])
def get_user(user_id: int):
return {"id": user_id}
The original global decorator API remains available:
from vedrakit import get, run
@get("/health-check")
def health_check():
return {"ok": True}
run()
A route can return a dictionary, a string, or (status_code, content):
@app.route("/created", ["POST"])
def created():
return 201, {"created": True}
Supported HTTP methods are GET, POST, PUT, PATCH, and DELETE.
Unsupported methods receive 405 Method Not Allowed with an Allow header.
Unknown paths receive 404.
Request and response models
BaseModel validates annotated fields and converts common primitive types.
Fields without a class default are required:
class SearchRequest(BaseModel):
phrase: str
page: int
include_archived: bool = False
@app.route("/search", ["POST"])
def search(request: SearchRequest):
return {
"phrase": request.phrase,
"page": request.page,
"include_archived": request.include_archived,
}
JSON requests must use Content-Type: application/json:
curl -X POST http://127.0.0.1:8080/search \
-H 'Content-Type: application/json' \
-d '{"phrase":"books","page":1,"include_archived":false}'
Use response_model to validate successful responses:
class UserResponse(BaseModel):
id: int
username: str
@app.route("/users/{user_id}", ["GET"], response_model=UserResponse)
def user(user_id: int):
return {"id": user_id, "username": "Ada"}
Use Query for documented, optional query parameters:
@app.route("/items", ["GET"])
def items(skip: int = Query(default=0), limit: int = Query(default=20)):
return {"skip": skip, "limit": limit}
Middleware, exceptions, and dependencies
Middleware runs before routing. Return False to stop a request with 403:
@app.middleware
def require_internal_header(request):
return request.headers.get("X-Internal") == "yes"
Custom exception handlers are registered by exception type:
@app.exception_handler(ValueError)
def bad_request(error):
return 422, {"error": str(error), "type": "validation_error"}
Dependencies can use the current request and inject their result into the handler:
from vedrakit import depends
def request_id(req):
return req.headers.get("X-Request-ID", "generated-locally")
@app.route("/request-info", ["GET"])
@depends(request_id)
def request_info(request_id):
return {"request_id": request_id}
Dependencies can also be used for authentication; the framework makes the
authenticated payload available as req.request_context["user"].
Authentication and RBAC
Vedrakit provides:
- PBKDF2-HMAC-SHA256 password hashes with a per-password random salt
- Constant-time password verification
- HS256 JWT issuance and verification
- Expiration (
exp) and issued-at (iat) claims - Route-level authentication
- Role checks for
admin,user,guest, andmoderator
from vedrakit import Role, Security
password_hash = Security.hash_password("a long password")
assert Security.verify_password("a long password", password_hash)
token = Security.create_jwt({"user_id": 123, "role": "admin"})
@app.route(
"/admin",
["GET"],
require_auth=True,
required_roles=[Role.ADMIN],
)
def admin_area():
return {"access": "granted"}
Send the token as:
Authorization: Bearer <token>
Older prototype SHA-256 password hashes can still be verified so existing accounts can be migrated on login. New hashes always use PBKDF2.
Production configuration
The core reads these environment variables:
| Variable | Default | Purpose |
|---|---|---|
SECRET_KEY |
unset | Application secret reserved for integrations |
JWT_SECRET |
unset | HS256 signing key |
DATABASE_URL |
sqlite:///app.db |
SQLite or PostgreSQL URL |
JWT_EXPIRE_MINUTES |
30 |
Token lifetime |
CORS_ORIGINS |
unset | Comma-separated allowed origins |
RATE_LIMIT_REQUESTS |
100 |
Requests per client/endpoint window |
RATE_LIMIT_WINDOW |
3600 |
Rate-limit window in seconds |
REDIS_URL |
unset | Optional Redis URL |
GRPC_PORT |
50051 |
Optional gRPC service port |
WEBSOCKET_PORT |
8765 |
Optional WebSocket service port |
PROMETHEUS_PORT |
9090 |
Optional standalone metrics port |
For production=True, Config.validate_production() requires non-empty
SECRET_KEY, JWT_SECRET, and an explicit CORS_ORIGINS list without *:
export SECRET_KEY='use-a-secret-manager'
export JWT_SECRET='use-a-different-secret'
export CORS_ORIGINS='https://app.example.com'
python -c 'from vedrakit import run; run(production=True, port=8080)'
Bind production servers to 0.0.0.0; development defaults to 127.0.0.1.
Do not commit these secrets or use the development fallback in a public
deployment.
SQLite and PostgreSQL models
The same annotated Model API works with SQLite and PostgreSQL. Select the
backend using DATABASE_URL:
# Local development
export DATABASE_URL='sqlite:///app.db'
# Production PostgreSQL
export DATABASE_URL='postgresql://app_user:strong-password@db.example.com:5432/app'
PostgreSQL uses the optional psycopg 3 driver:
pip install -e ".[postgresql]"
Models use annotated fields and backend-appropriate types and identity columns:
from vedrakit import Database, Migration, Model
class Product(Model):
id: int
name: str
price: float
Product.create_table()
product = Product(name="Notebook", price=12.5).save()
same_product = Product.get(product.id)
all_products = Product.all()
Migrations are idempotent and recorded in the migrations table:
Migration.init_migration_table()
Migration.add_migration(
"add-product-index",
"CREATE INDEX product_name_idx ON products(name)",
)
Migration.revert_migration(
"add-product-index",
"DROP INDEX product_name_idx",
)
Call Database.close_all() during controlled shutdown or test teardown.
Connections are scoped per worker thread so PostgreSQL connections are not
shared across HTTP worker threads. PostgreSQL migrations support the same
idempotent Migration.add_migration() and revert_migration() API. Keep
migration scripts to discrete SQL statements separated by semicolons.
Caching and rate limiting
@cache supports both synchronous and asynchronous functions. Redis is used
when REDIS_URL and the optional redis package are available. If Redis is
unavailable, the cache and rate limiter use a process-local, thread-safe
fallback so a development server remains usable:
from vedrakit import cache
@cache(timeout=300)
def expensive_lookup(user_id: int):
return {"user_id": user_id}
The in-memory fallback is not shared between processes. Use Redis for multi-worker deployments.
Background work
TaskQueue uses worker threads and supports both sync and async callables:
import asyncio
from vedrakit import TaskQueue, background_task
queue = TaskQueue("emails")
queue.start_workers(2)
asyncio.run(queue.add_task(send_email, "user@example.com"))
@background_task
def rebuild_index():
...
Call queue.stop() during shutdown. The default background_task decorator
uses the started default queue and otherwise creates a daemon thread.
Background exceptions are logged and do not crash the HTTP worker.
Static files and CORS
App(static_dir="static") serves files below /static/. The real path is
checked before opening a file, so ../ traversal cannot escape the static
root. Missing files return 404.
CORS responses are only allowed for origins in Config.CORS_ORIGINS. In
production, use explicit origins rather than *, especially when cookies or
authorization headers are involved. HTTP responses also include
X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and a strict
Referrer-Policy by default.
OpenAPI documentation
GET /docs returns an OpenAPI 3.0.3 JSON document generated from registered
routes. It includes:
- Route methods and summaries from docstrings
- Path and query parameters
- JSON request bodies for
BaseModelinputs - Model schemas and required fields
The document is intentionally returned as JSON so it can be consumed by Swagger UI, Redoc, code generators, or an API gateway.
GraphQL, WebSocket, and gRPC
These integrations are optional so the core package remains dependency-light.
GraphQL
Install graphene, create a schema, and register it:
import graphene
from vedrakit import graphql_schema
class Query(graphene.ObjectType):
hello = graphene.String()
def resolve_hello(self, info):
return "Hello from Vedrakit"
@graphql_schema(graphene.Schema(query=Query))
def api_schema():
pass
POST a GraphQL JSON body to /graphql:
curl -X POST http://127.0.0.1:8080/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ hello }"}'
WebSocket
Install websockets, register a handler, and run the optional service:
from vedrakit import WebSocketManager, run_websocket_server, websocket_endpoint
@websocket_endpoint("/ws")
async def echo(websocket, path):
WebSocketManager.add_connection(path, websocket)
try:
async for message in websocket:
await websocket.send(f"Echo: {message}")
finally:
WebSocketManager.remove_connection(path, websocket)
run_websocket_server()
gRPC
Install grpcio and pass generated bindings to the server:
from vedrakit import run_grpc_server
from generated_pb2_grpc import add_MyServiceServicer_to_server
server = run_grpc_server(
servicer=MyService(),
add_servicer=add_MyServiceServicer_to_server,
)
server.wait_for_termination()
The prototype's previous gRPC placeholder has been replaced with a real
server factory; service definitions remain application-specific and should be
generated from your .proto files.
Metrics and operations
The framework records request count, active requests, and request duration.
The built-in /metrics endpoint emits Prometheus-compatible text. A separate
daemon metrics server is also available:
from vedrakit import run_metrics_server
run_metrics_server(port=9090)
Health/readiness semantics:
/healthchecks process liveness and does not require the database/readyperformsSELECT 1against the configured database
Testing
Run the complete suite:
python -m unittest discover -s tests -v
python -m py_compile vedrakit/*.py
The tests cover HTTP behavior through real local sockets, not only direct function calls. They cover validation, response models, query and path coercion, async routes, auth and RBAC, status codes, CORS, static traversal, OpenAPI, readiness, password/JWT security, SQLite CRUD, migrations, cache fallback, and task workers.
Package layout
vedrakit/
__init__.py Public API
core.py Runtime implementation
examples/
basic_app.py Runnable example
tests/
test_vedrakit.py End-to-end unit tests
pyproject.toml Package metadata and optional extras
License
MIT
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 vedrakit-1.0.0.tar.gz.
File metadata
- Download URL: vedrakit-1.0.0.tar.gz
- Upload date:
- Size: 33.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e0bf395212a828309338413db1c79481b7cc7ee82da40d4c48351269050091ef
|
|
| MD5 |
c556f5dae33b2a4e9271d0567397a203
|
|
| BLAKE2b-256 |
574736ab525fd550c644cb403235cdc23295123a6fb61cf2d5fbe2faff0c6b20
|
File details
Details for the file vedrakit-1.0.0-py3-none-any.whl.
File metadata
- Download URL: vedrakit-1.0.0-py3-none-any.whl
- Upload date:
- Size: 23.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e1d6d51d653100e24236dc02380304c47a396166e114bf4c54bc219f638eecde
|
|
| MD5 |
559a689a48d29553ce96d257695be652
|
|
| BLAKE2b-256 |
8d82511e52ed27fcee5c1c3fdd103e37d25654c224a2d5b52f69460e627a3c16
|