A modern async JSON-RPC framework built on Starlette. Where determination rises from simplicity.
Project description
wilrise
Wilrise is a server-side JSON-RPC framework built on Starlette. It targets teams that want RPC semantics (method-oriented API, batch support) while staying inside the ASGI ecosystem—e.g. mount under an existing Starlette/FastAPI app, reuse middleware, and use async throughout. Simple to get started; see docs/ for production topics (errors, configuration, observability, versioning, runbook, architecture).
This document assumes you are already familiar with JSON-RPC 2.0 (request/response format, method, params, id, error codes). The framework implements the server side only; use any JSON-RPC 2.0–compliant client to call your methods.
Install
# With uv (recommended)
uv add wilrise
# Or pip
pip install wilrise
After installing, use from wilrise import Wilrise in your code. For parameter validation (clear -32602 instead of -32603 on type errors), install the optional extra: uv add "wilrise[pydantic]" or pip install "wilrise[pydantic]".
Quick start
1. Write a minimal service (e.g. main.py):
from wilrise import Wilrise
app = Wilrise()
@app.method
def add(a: int, b: int) -> int:
return a + b
# Recommended: app.run() uses uvicorn with access_log=False and built-in JSON-RPC style logs
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8000)
2. Run the server (from the directory that contains main.py):
uv run python main.py
You’ll see JSON-RPC style logs (e.g. JSON-RPC add → 200 in 12.50ms) instead of generic "POST / 200" lines. To use another ASGI server or mount under an existing app, use app.as_asgi() (see Mounting on an existing app below).
If you use the repo’s examples, from project root:
uv run --project examples python examples/minimal.py
3. Send a request (single call):
curl -X POST http://127.0.0.1:8000/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"add","params":{"a":1,"b":2},"id":1}'
Example response: {"jsonrpc":"2.0","result":3,"id":1}.
- Request body must be JSON with
"jsonrpc":"2.0","method"(method name), optional"params"(object or array), and optional"id"(omit for notifications; no response body).
Batch requests
Send an array of JSON-RPC requests in one POST; the response is an array of responses in the same order. Notifications (no id) do not get an entry in the response array. If all requests are notifications, the server returns 204 No Content. One failed request does not fail the whole batch; each request gets its own success or error response.
Advanced: dependency injection & routers
Use Router and include_router(router, prefix="...") to group methods by module (e.g. math.add, user.get). See examples/main.py for a full pattern.
from wilrise import Router, Use, Wilrise
from starlette.requests import Request
app = Wilrise()
# Dependency provider (e.g. from request, connection pool)
async def get_db_session(request: Request):
return DBSession() # e.g. from pool, request.state, etc.
@app.method
async def add(a: int, b: int) -> int:
return a + b
@app.method
async def get_user(user_id: int, db: DBSession = Use(get_db_session)) -> dict | None:
return await db.get_user(user_id) # db is injected by Use
# If Use(provider) raises, the request returns -32603 (Internal error); treat as dependency failure.
# Standalone: app.run(); mounting: app.as_asgi()
Prerequisites
- Built on Starlette; if you know ASGI or FastAPI, you’ll feel at home (
@app.method≈ route,Use≈ dependency injection — similar to FastAPI’sDepends). - Without the optional Pydantic extra, parameter types are not validated; wrong types (e.g. passing a string where an int is expected) lead to Internal error (-32603) or unexpected behavior. To get clear Invalid params (-32602) on type errors, install:
uv add "wilrise[pydantic]"(see Pydantic (optional) below).
Configuration
Wilrise accepts these init options:
- debug (default
False): WhenTrue, error responses include full exception info. KeepFalsein production to avoid leaking sensitive data. - max_batch_size (default
50): Max number of requests in a batch; excess returns -32600. - max_request_size (default
1024*1024, 1MB): Max request body size in bytes; only checked whenContent-Lengthis present—requests without this header are not size-limited by the framework. Excess returns 413. For strict limits regardless of headers (e.g. production), use a reverse proxy or custom middleware. - log_requests (default
True): WhenTrue, each request is logged in JSON-RPC style: method name(s), HTTP status, and duration (e.g.JSON-RPC math.add → 200 in 12.50ms; for batches,JSON-RPC batch(n) [method1, ...] → 200 in 45ms). - logger (default
None): Logger to use for request and error logs. IfNone, useslogging.getLogger("wilrise.core"). Pass a custom logger (e.g.logging.getLogger("app.rpc")) to control level and handlers per app. - log_level (default
None): If set, calllogger.setLevel(log_level)so only messages at or above this level are emitted (e.g.logging.WARNINGin production to suppress INFO success logs but keep ERROR).
You can pass options in code or load them from environment variables (e.g. WILRISE_DEBUG, WILRISE_MAX_BATCH_SIZE, WILRISE_LOG_LEVEL). See docs/configuration.md for env var names, default/dev/production presets, and the optional from_env() helper.
# Production (recommend from env in deployment)
from wilrise import Wilrise, from_env
app = Wilrise(**from_env())
# Or explicitly
app = Wilrise(debug=False, max_batch_size=50, max_request_size=1024 * 1024)
# Development: enable debug for troubleshooting
app = Wilrise(debug=True)
Running the server
- Recommended:
app.run(host="127.0.0.1", port=8000)— runs uvicorn withaccess_log=Falseso only JSON-RPC style logs (method, status, duration) are printed. Optional args:app.run(host="0.0.0.0", port=8000, access_log=True, **uvicorn_kwargs). - Alternative:
uvicorn.run(app.as_asgi(), host="127.0.0.1", port=8000, access_log=False)if you need to pass other uvicorn options. - Multi-worker: The app is stateless; use multiple uvicorn workers (e.g.
uvicorn ... --workers 4). Shared resources (DB pool, Redis) should be initialized in startup and closed in shutdown so each worker has its own connections. Production: run behind a process manager (e.g. gunicorn + uvicorn worker), setdebug=False, and configure via environment variables. - Mounting: Use
app.as_asgi()to get the Starlette app and mount it under another application (see Mounting on an existing app below).
Middleware
Add Starlette middleware (CORS, auth, logging, etc.) with add_middleware:
from starlette.middleware.base import BaseHTTPMiddleware
app = Wilrise()
class CustomMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# Before request...
response = await call_next(request)
# After response...
return response
app.add_middleware(CustomMiddleware)
Example project
examples/ is a demo project with the same layout as using wilrise in a new project. Recommended order: minimal.py → main.py → auth_crud/ (see examples/README.md).
examples/
├── pyproject.toml # depends on wilrise
├── minimal.py # Minimal: single add method + run
├── main.py # Full: Router, Param, Use, aliases, etc.
└── auth_crud/ # Full app: SQLAlchemy, JWT auth, CRUD (whole-params style)
-
Minimal (only
add):cd examples && uv sync && uv run python minimal.py
-
Full (Router, DI, Param aliases, etc.):
cd examples && uv sync && uv run python main.py
-
Auth + CRUD (login, user CRUD; run
uv syncfirst):cd examples && uv sync && uv run python -m auth_crud.main
From repo root:
uv run --project examples python examples/minimal.py
# or
uv run --project examples python examples/main.py
# or
uv run --project examples python -m auth_crud.main
For local development,
pyproject.tomlpoints at the local wilrise via[tool.uv.sources]. Remove that section when installing from PyPI.
Pydantic (optional)
Without the pydantic extra, parameter types (e.g. int, str) are not validated; wrong types may lead to -32603 or unexpected behavior. For declarative validation, install:
uv add "wilrise[pydantic]"
# or pip install "wilrise[pydantic]"
- Single param of type BaseModel: Two valid ways to call; recommended is whole params (the entire
paramsobject is the model). Clients and server should agree on one style per method.- Whole params (recommended):
"params": {"a": 1, "b": 2}→ validated asAddParams. - Keyed:
"params": {"params": {"a": 1, "b": 2}}→ only the value for the keyparamsis validated.
- Whole params (recommended):
- Multiple params: Any param annotated with a
BaseModelhas its JSON validated and deserialized to that model. - On validation failure,
-32602(Invalid params) is returned; all-32602responses useerror.data.validation_errors(list of{loc, msg, type, ...}) for consistent client handling. - Return values must be JSON-serializable; otherwise the server returns
-32603.
Example:
from pydantic import BaseModel
from wilrise import Wilrise
app = Wilrise()
class AddParams(BaseModel):
a: int
b: int
@app.method
def add(params: AddParams) -> int:
return params.a + params.b
Both requests below return result: 3. Use the recommended whole-params style and keep it consistent with your client.
# Whole params object (recommended for single BaseModel param)
curl -X POST http://127.0.0.1:8000/ -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"add","params":{"a":1,"b":2},"id":1}'
# Keyed: params.params
curl -X POST http://127.0.0.1:8000/ -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"add","params":{"params":{"a":1,"b":2}},"id":1}'
Development and code style
Install dev dependencies first:
uv sync --group dev
Formatting & linting — Ruff:
- Format:
uv run ruff format . - Lint:
uv run ruff check . - Both:
uv run ruff format . && uv run ruff check .
Type checking — Pyright (strict mode):
- Type check:
uv run pyright
Configuration in [tool.ruff] and [tool.pyright] in pyproject.toml. Run the above before committing to keep style and types consistent. For full setup, tests, and how to contribute, see CONTRIBUTING (中文).
Contributing
Bug reports, feature ideas, and pull requests are welcome. See CONTRIBUTING for development setup, code style, and PR guidelines (中文).
Error codes (JSON-RPC 2.0)
- -32700 Parse error — request body is not valid JSON.
- -32600 Invalid Request — missing
jsonrpc/method, method not string, params not object/array, body too large, batch over limit, etc. - -32601 Method not found — called method is not registered.
- -32602 Invalid params — missing required param, Pydantic validation failed, etc. All
-32602responses useerror.data.validation_errors(list of{loc, msg, type}) for consistent client handling. - -32603 Internal error — method or dependency (Use) raised an exception, or result not JSON-serializable (details hidden in production). In production, use
set_exception_mapperto map known exceptions (e.g. DB, auth) to specific codes so clients get actionable errors instead of a generic "Internal error".
For application-level errors (-32099..-32000), raise RpcError(code, message, data=...) in your method or Use provider; use data (e.g. data={"code": "auth_failed"}) for stable client handling. For third-party exceptions (DB, HTTP client), use set_exception_mapper(mapper) to map them to a protocol or application code instead of exposing -32603. See docs/errors.md for error layering, retriable guidance, and ExceptionMapper vs RpcError order.
Mounting on an existing app
Mount the JSON-RPC app on a path (e.g. /rpc) in Starlette or FastAPI:
from starlette.applications import Starlette
from starlette.routing import Mount
from wilrise import Wilrise
rpc = Wilrise()
@rpc.method
def add(a: int, b: int) -> int:
return a + b
# Mount on /rpc; requests go to http://host/rpc
app = Starlette(routes=[Mount("/rpc", app=rpc.as_asgi())])
# Then: curl -X POST http://127.0.0.1:8000/rpc -H "Content-Type: application/json" \
# -d '{"jsonrpc":"2.0","method":"add","params":{"a":1,"b":2},"id":1}'
With FastAPI: app.mount("/rpc", rpc.as_asgi()).
Param and Use
- Param(description=..., alias=...):
descriptionis metadata only (e.g. for your own docs or OpenRPC);aliaslets the client send a different key (e.g.userIdinstead ofuser_id). - Use(provider): Injects the provider’s return value. If the provider raises, the request returns
-32603(Internal error); treat it as dependency failure (e.g. DB or auth).
Observability
- Logging: Configure the
wilriselogger (e.g.logging.getLogger("wilrise").setLevel(logging.INFO)) to control level and handlers. The framework logs each request withextra(request_id, rpc_methods, status_code, duration_ms). For JSON/structured logs or custom sinks, useadd_request_logger(logger_fn). To use Loguru as the log backend, see the tutorial in docs/observability.md. - Request ID: Set the
X-Request-IDheader at the gateway or client; it is available asRpcContext.http_request_idand in log extras for correlation. - Tracing / metrics: docs/observability.md describes optional OpenTelemetry and metrics patterns via
add_request_loggeror middleware.
Version and compatibility
- Versioning: Semantic Versioning (MAJOR.MINOR.PATCH). Current status: Beta (0.1.x); before 1.0 we may make breaking changes; from 1.0 we keep backward compatibility within each MAJOR. See docs/versioning.md and CHANGELOG.md.
- Upgrades: When we release a new MAJOR or significant MINOR, we add upgrade notes under
docs/(e.g.docs/upgrade-1.0.md).
FAQ
-
How can clients discover available methods?
JSON-RPC 2.0 does not require a schema; this framework does not ship OpenAPI or built-in method discovery. You can add your own RPC method that returns the list of method names (and param docs if you maintain them), or serve OpenRPC / self-describing docs. -
Why Python 3.12+?
The project targets a single modern version for type hints, performance, and maintainability; support for older Python versions is not planned for now.
Project structure
- wilrise — Core implementation; optional
from_envinwilrise.config. - examples/ — Demo project (minimal + full example).
- docs/ — Production topics: errors, configuration, observability, versioning, runbook, architecture, production checklist.
中文说明见 README.zh-CN.md。
Project details
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 wilrise-0.1.0.tar.gz.
File metadata
- Download URL: wilrise-0.1.0.tar.gz
- Upload date:
- Size: 19.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
55bb9a02fd1deebffec1c626544d7ba673aae40488e1bab54da09baffdaf01bd
|
|
| MD5 |
b2fb3138600ad25ee590ac37ea147557
|
|
| BLAKE2b-256 |
0b78e8abd5c6598270373fe7a69e077b28087903322f43e9fb0660e397f22b48
|
File details
Details for the file wilrise-0.1.0-py3-none-any.whl.
File metadata
- Download URL: wilrise-0.1.0-py3-none-any.whl
- Upload date:
- Size: 22.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f4767dcf9d340f922ed8c77231492b41e65ae669564ac6d75b33ca802fe757a
|
|
| MD5 |
dcbf509f876eb8abe321287d04462ae0
|
|
| BLAKE2b-256 |
c12edb82420d8779e7aca9f85405e16c413a32ae0cc3eef99b814d7e30e9b3ce
|