Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,29 @@ jobs:
- name: Metadata must not link to hijacked domains
run: python .github/scripts/check_metadata.py dist/*

docs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- run: python -m pip install -e . -r requirements/docs.txt -r requirements/examples.txt
- run: mkdocs build --strict
- name: Examples in the README and docs, with every optional library installed
run: |
for f in README.md docs/*.md; do
python tests/doc_examples.py "$f"
done
- name: Start each example server and send it requests
run: python docs/examples/check_examples.py

ci-ok:
name: CI OK
if: always()
needs: [test, lint, package]
needs: [test, lint, package, docs]
runs-on: ubuntu-24.04
steps:
- name: All jobs passed
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Docs

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- run: python -m pip install -r requirements/docs.txt
- run: mkdocs build --strict
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: site

deploy:
needs: build
runs-on: ubuntu-24.04
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ error instead. If you depend on the old output, pass `serializer=json.dumps`.
follow_untyped_imports = true
```

### Documentation

The documentation now lives in this repository, under `docs/`, and is
published at https://bensynapse.github.io/jsonrpcserver/. The framework
examples moved there from the wiki. CI starts each example server and sends
it requests, and runs every code example in the docs. The websockets example
uses the current `websockets.asyncio` API (#287). A new Security page covers
the settings to check before exposing a server.

### Deprecations

Three functions in `jsonrpcserver.response` have new names. The old names still
Expand Down
48 changes: 29 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,63 @@
<p align="center">
<img src="https://img.shields.io/pypi/v/jsonrpcserver.svg" alt="PyPI" />
<img src="https://github.com/explodinglabs/jsonrpcserver/actions/workflows/code-quality.yml/badge.svg" alt="Code Quality" />
<img src="https://coveralls.io/repos/github/explodinglabs/jsonrpcserver/badge.svg?branch=main" alt="Coverage Status" />
<a href="https://pypi.org/project/jsonrpcserver/"><img src="https://img.shields.io/pypi/v/jsonrpcserver.svg" alt="PyPI" /></a>
<a href="https://github.com/bensynapse/jsonrpcserver/actions/workflows/ci.yml"><img src="https://github.com/bensynapse/jsonrpcserver/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
<img src="https://img.shields.io/pypi/pyversions/jsonrpcserver" alt="Python versions" />
<img src="https://img.shields.io/pypi/dw/jsonrpcserver" alt="Downloads" />
<img src="https://img.shields.io/pypi/l/jsonrpcserver.svg" alt="License" />
<img src="https://img.shields.io/github/license/bensynapse/jsonrpcserver" alt="License" />
</p>

<p align="center">
<img alt="Jsonrpcserver Logo" src="https://github.com/explodinglabs/jsonrpcserver/blob/main/logo.png?raw=true" />
<img alt="Jsonrpcserver Logo" src="https://raw.githubusercontent.com/bensynapse/jsonrpcserver/main/logo.png" />
</p>

<p align="center">
<i>Process incoming JSON-RPC requests in Python</i>
</p>

<p align="center">
<a href="https://explodinglabs.com/jsonrpcserver/">Documentation</a> |
<a href="https://github.com/explodinglabs/jsonrpcserver/wiki">Developer Wiki</a>
<a href="https://bensynapse.github.io/jsonrpcserver/">Documentation</a> |
<a href="https://bensynapse.github.io/jsonrpcserver/examples/">Examples</a> |
<a href="https://github.com/bensynapse/jsonrpcserver/blob/main/CHANGELOG.md">Changelog</a>
</p>

https://github.com/user-attachments/assets/94fb4f04-a5f1-41ca-84dd-7e18b87990e0

## 🚀 Installation
## Installation

```sh
pip install jsonrpcserver
```

## ⚒️ Usage
It supports Python 3.8 and later.

## Usage

```python
from jsonrpcserver import dispatch, method, Success
from jsonrpcserver import Result, Success, dispatch, method


@method
def ping():
def ping() -> Result:
return Success("pong")
```


response = dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}')
# => '{"jsonrpc": "2.0", "result": "pong", "id": 1}'
```python
>>> dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}')
'{"jsonrpc": "2.0", "result": "pong", "id": 1}'
```

## 📚 Documentation
jsonrpcserver doesn't listen on a port itself. The
[examples](https://bensynapse.github.io/jsonrpcserver/examples/) show it with
Flask, FastAPI, Django, aiohttp, websockets, ZeroMQ and more.

Before you expose a server to the internet, read the
[security notes](https://bensynapse.github.io/jsonrpcserver/security/).

👉 Full documentation is at [explodinglabs.com/jsonrpcserver/](https://explodinglabs.com/jsonrpcserver/).
## Documentation

👉 See the [Wiki](https://github.com/explodinglabs/jsonrpcserver/wiki) for community-editable usage examples in various frameworks, libraries, and transport protocols.
Full documentation is at
[bensynapse.github.io/jsonrpcserver](https://bensynapse.github.io/jsonrpcserver/).

## ➡️ See Also
## See also

- [jsonrpcclient](https://github.com/explodinglabs/jsonrpcclient) – Create JSON-RPC requests and parse responses in Python
- [jsonrpcclient](https://github.com/bensynapse/jsonrpcclient): create JSON-RPC requests and parse responses in Python
106 changes: 106 additions & 0 deletions docs/assets/extra.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
:root {
/* ✅ Fonts */
--md-text-font: -apple-system, BlinkMacSystemFont, "Segoe UI Adjusted",
"Segoe UI", "Liberation Sans", sans-serif;
--md-code-font: "SF Mono", SFMono-Regular, ui-monospace, "DejaVu Sans Mono",
Menlo, Consolas, monospace;

/* ✅ Light Mode Text Colors */
--md-default-fg-color: #111;
}

[data-md-color-scheme="slate"] {
/* ✅ Dark Mode Text Colors */
--md-default-fg-color: #e9e9e9;
}

/* Fix a horizontal scroll isue */
.md-typeset * {
margin-left: 0 !important;
margin-right: 0 !important;
}

.md-typeset {
margin-left: 0 !important;
margin-right: 0 !important;

p,
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
padding: 0;
padding-left: 20px;
padding-right: 20px;
}

/* Change header fonts */
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
font-style: normal;
letter-spacing: -0.5px;
line-height: 140%;
}

h1 {
color: var(--md-default-fg-color);
font-size: 1.2rem;
margin-bottom: 1em;
}

h2 {
font-size: 1rem;
}

h3 {
font-size: 0.9rem;
}

p, ul, ol, pre {
font-size: 17px;
margin-top: 1em;
margin-bottom: 1em;
}

ul, ol {
margin-left: 3rem;
padding-left: 3rem;
}

blockquote {
border-left: 0 !important;
border-radius: 0;
margin: 0;
margin-bottom: 1em;
padding: 1px;
background-color: rgba(255, 87, 34, 0.05);

[data-md-color-scheme="slate"] {
background-color: rgba(255, 204, 0, 0.1);
}
}

pre {
line-height: 150%;
}
pre > code {
padding-left: 20px;
}
}

.md-content__inner {
/* Target only the page title (first H1 in the article) */
.md-typeset {
h1:first-of-type {
padding-left: 20px;
}
}
}

article {
padding-bottom: 3rem;
}

Binary file added docs/assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 82 additions & 0 deletions docs/async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Async

For asyncio servers, use `async_dispatch`. Methods can be `async`, and their
`await`s don't block the server.

```python
import asyncio

from jsonrpcserver import Result, Success, async_dispatch, method


@method
async def ping() -> Result:
return Success("pong")
```

```python
>>> asyncio.run(async_dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}'))
'{"jsonrpc": "2.0", "result": "pong", "id": 1}'
```

In a real server you're already inside a coroutine, so write
`response = await async_dispatch(request)`.

`async_dispatch` takes the same options as [`dispatch`](dispatch.md).
`async_dispatch_to_serializable` and `async_dispatch_to_response` are the async
versions of the other two functions.

Plain functions work as methods too, but they run on the event loop, so a slow
one holds up everything else. Async methods don't work with the synchronous
`dispatch`. The client gets an Internal error, and the log says to use
`async_dispatch`.

## Batches run concurrently

The requests in a batch run at the same time. These ten requests take about a
tenth of a second, not one second:

```python
import json
import time


@method
async def wait() -> Result:
await asyncio.sleep(0.1)
return Success()


batch = json.dumps([{"jsonrpc": "2.0", "method": "wait", "id": n} for n in range(10)])
```

```python
>>> start = time.monotonic()
>>> responses = json.loads(asyncio.run(async_dispatch(batch)))
>>> len(responses)
10
>>> time.monotonic() - start < 0.5
True
```

Every request in a batch runs at once, so set `max_batch_size` if clients
can send big batches. See [Security](security.md).

## Notifications

A notification is a request with no `id`. The spec says not to respond to it,
so `async_dispatch` gives an empty string:

```python
>>> asyncio.run(async_dispatch('{"jsonrpc": "2.0", "method": "ping"}'))
''
```

With HTTP you have to send something, so send an empty body with status 204.
With websockets or a message queue, you can skip sending:

```python
async def handle(request: str, send) -> None:
if response := await async_dispatch(request):
await send(response)
```
Loading