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 vedrakitCLI for scaffolding, development, route inspection, OpenAPI export, and typed TypeScript client generation- 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
The package installs a dependency-free CLI:
vedrakit --help
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
CLI and project scaffolding
Create a complete starter project with an application, tests, metadata, environment template, and README:
vedrakit new inventory-api
cd inventory-api
python -m pip install -e .
vedrakit dev app:app
Useful commands:
vedrakit routes app:app
vedrakit openapi app:app --output openapi.json
vedrakit client app:app --output api-client.ts
The dev command accepts --host, --port, and --production. Targets use
the module:attribute format, so a file target such as app.py:app also works.
The generated TypeScript client uses the browser fetch API and has no runtime
dependency.
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 and vedrakit openapi return an OpenAPI 3.0.3 JSON document
generated from registered routes. Configure application metadata:
app = App(
title="Inventory API",
version="2.0.0",
description="Manage inventory items.",
servers=[{"url": "https://api.example.com"}],
)
The document includes:
- Route methods and summaries from docstrings
- Stable operation IDs, tags, descriptions, and deprecation flags
- Path and query parameters with types, defaults, descriptions, and examples
- Nested JSON request/response schemas for
BaseModel, lists, unions, enums, dictionaries, and common primitive types - JWT bearer security requirements and role metadata for protected routes
- Model schemas and required fields
Routes can override generated metadata:
@app.get(
"/items/{item_id}",
summary="Read an item",
tags=["items"],
operation_id="getItem",
response_description="The requested item",
)
def get_item(item_id: int):
"""Return one item by ID."""
return {"id": item_id}
The document is intentionally returned as JSON so it can be consumed by Swagger UI, Redoc, code generators, or an API gateway.
Typed client generation
Generate a TypeScript client from the same OpenAPI document:
vedrakit client examples.complete_app:app --output examples/todos-client.ts
The output contains typed interfaces for model schemas, one method per
operation, path/query serialization, JSON request bodies, and an ApiError
class:
import { createApiClient } from "./todos-client";
const api = createApiClient({ baseUrl: "http://127.0.0.1:8080" });
const response = await api.listTodos({ limit: 10 });
The generator is also available as a Python API:
from vedrakit import generate_typescript_client
typescript = generate_typescript_client(app.openapi())
Complete examples
examples/basic_app.py demonstrates the smallest validated API.
examples/complete_app.py demonstrates a dependency-free CRUD API with
request/response models, path and query parameters, correct status codes,
OpenAPI tags, and client generation commands.
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
__main__.py `python -m vedrakit` entry point
cli.py CLI and project scaffolding
codegen.py Typed client generators
core.py Runtime implementation
examples/
basic_app.py Runnable example
complete_app.py Complete CRUD example
tests/
test_vedrakit.py End-to-end runtime coverage
test_tooling.py CLI, OpenAPI, and client generator coverage
pyproject.toml Package metadata and optional extras
Releasing to PyPI
Releases are published automatically by GitHub Actions when a strict
vMAJOR.MINOR.PATCH tag matches the version in pyproject.toml.
Before the first automated release, configure a PyPI Trusted Publisher for this GitHub repository:
- Open the Vedrakit project on PyPI and add a GitHub trusted publisher.
- Set the owner to
dhaval-vedra, the repository toVEDRAKIT, and the workflow filename topublish.yml. - Leave the environment blank unless the workflow is later updated to use a GitHub deployment environment.
To publish a future version, update the project version, run the test suite, commit the change, and push the matching tag:
# pyproject.toml: [project] version = "1.1.0"
python -m unittest discover -s tests -v
git add pyproject.toml
git commit -m "Release v1.1.0"
git tag v1.1.0
git push origin main v1.1.0
The workflow checks that the tag and package metadata agree, builds both a
wheel and source distribution, validates their metadata, and publishes through
PyPI's OIDC trusted-publishing flow. Build output stays in the ignored dist/
directory and is never committed.
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.1.0.tar.gz.
File metadata
- Download URL: vedrakit-1.1.0.tar.gz
- Upload date:
- Size: 45.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3302f20e0c645dcd85ebf1f8036ef16be093a5939e59d6e3c72491be50668e95
|
|
| MD5 |
043251cec404554aa349c6c845eb6c70
|
|
| BLAKE2b-256 |
b8bf345c9c6470487e2eb6c47caec9155c4497e24e132cec71f96343e5cb4fe4
|
File details
Details for the file vedrakit-1.1.0-py3-none-any.whl.
File metadata
- Download URL: vedrakit-1.1.0-py3-none-any.whl
- Upload date:
- Size: 33.8 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 |
d32a84c4710f67ec353cd001329058696b1abccc70c6a529d25d2f88c6e66fca
|
|
| MD5 |
5509641070177d2f3661a25cd5863e0a
|
|
| BLAKE2b-256 |
fd38fce0e5aff518f35e917368335e94b824cc43054057d39bb0926fa55a8335
|