Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace readthedocs with mkdocs #282

Merged
merged 2 commits into from
Aug 16, 2024
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<img
alt="jsonrpcserver"
style="margin: 0 auto;"
src="https://github.com/explodinglabs/jsonrpcserver/blob/main/docs/logo.png?raw=true"
src="https://github.com/explodinglabs/jsonrpcserver/blob/main/logo.png?raw=true"
/>

![PyPI](https://img.shields.io/pypi/v/jsonrpcserver.svg)
Expand All @@ -16,7 +16,7 @@ pip install jsonrpcserver
```

```python
from jsonrpcserver import method, serve, Ok, Result
from jsonrpcserver import method, Result, Ok

@method
def ping() -> Result:
Expand Down
41 changes: 41 additions & 0 deletions docs/async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Async dispatch is supported.

```python
from jsonrpcserver import async_dispatch, async_method, Ok, Result

@async_method
async def ping() -> Result:
return Ok("pong")

await async_dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}')
```

Some reasons to use this:

- Use it with an asynchronous protocol like sockets or message queues.
- `await` long-running functions from your method.
- Batch requests are dispatched concurrently.

## Notifications

Notifications are requests without an `id`. We should not respond to
notifications, so jsonrpcserver gives an empty string to signify there is *no
response*.

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

If the response is an empty string, don't send it.

```python
if response := dispatch(request):
send(response)
```

```{note}
A synchronous protocol like HTTP requires a response no matter what, so we can
send back the empty string. However with async protocols, we have the choice of
responding or not.
```
84 changes: 84 additions & 0 deletions docs/dispatch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Dispatch

The `dispatch` function processes a JSON-RPC request, attempting to call the method(s)
and gives a JSON-RPC response.

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

It's a pure function; it will always give you a JSON-RPC response. No exceptions will be
raised.

[See how dispatch is used in different frameworks.](examples)

## Optional parameters

The `dispatch` function takes a request as its argument, and also has some optional
parameters that allow you to customise how it works.

### methods

This lets you specify the methods to dispatch to. It's an alternative to using
the `@method` decorator. The value should be a dict mapping function names to
functions.

```python
def ping():
return Ok("pong")

dispatch(request, methods={"ping": ping})
```

Default is `global_methods`, which is an internal dict populated by the
`@method` decorator.

### context

If specified, this will be the first argument to all methods.

```python
@method
def greet(context, name):
return Ok(f"Hello {context}")

>>> dispatch('{"jsonrpc": "2.0", "method": "greet", "params": ["Beau"], "id": 1}', context="Beau")
'{"jsonrpc": "2.0", "result": "Hello Beau", "id": 1}'
```

### deserializer

A function that parses the JSON request string. Default is `json.loads`.

```python
dispatch(request, deserializer=ujson.loads)
```

### jsonrpc_validator

A function that validates the request once the JSON string has been parsed. The
function should raise an exception (any exception) if the request doesn't match
the JSON-RPC spec (https://www.jsonrpc.org/specification). Default is
`default_jsonrpc_validator` which uses Jsonschema to validate requests against
a schema.

To disable JSON-RPC validation, pass `jsonrpc_validator=lambda _: None`, which
will improve performance because this validation takes around half the dispatch
time.

### args_validator

A function that validates a request's parameters against the signature of the
Python function that will be called for it. Note this should not validate the
_values_ of the parameters, it should simply ensure the parameters match the
Python function's signature. For reference, see the `validate_args` function in
`dispatcher.py`, which is the default `args_validator`.

### serializer

A function that serializes the response string. Default is `json.dumps`.

```python
dispatch(request, serializer=ujson.dumps)
```
39 changes: 39 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
## How to disable schema validation?

Validating requests is costly - roughly 40% of dispatching time is spent on schema validation.
If you know the incoming requests are valid, you can disable the validation for better
performance.

```python
dispatch(request, validator=lambda _: None)
```

## Which HTTP status code to respond with?

I suggest:

```python
200 if response else 204
```

If the request was a notification, `dispatch` will give you an empty string. So
since there's no http body, use status code 204 - no content.

## How to rename a method

Use `@method(name="new_name")`.

Or use the dispatch function's [methods
parameter](https://www.jsonrpcserver.com/en/latest/dispatch.html#methods).

## How to get the response in other forms?

Instead of `dispatch`, use:

- `dispatch_to_serializable` to get the response as a dict.
- `dispatch_to_response` to get the response as a namedtuple (either a
`SuccessResponse` or `ErrorResponse`, these are defined in
[response.py](https://github.com/explodinglabs/jsonrpcserver/blob/main/jsonrpcserver/response.py)).

For these functions, if the request was a batch, you'll get a list of
responses. If the request was a notification, you'll get `None`.
37 changes: 37 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Jsonrpcserver

Jsonrpcserver processes JSON-RPC requests.

## Quickstart

Install jsonrpcserver:
```python
pip install jsonrpcserver
```

Create a `server.py`:

```python
from jsonrpcserver import method, serve, Ok

@method
def ping():
return Ok("pong")

if __name__ == "__main__":
serve()
```

Start the server:
```sh
$ python server.py
```

Send a request:
```sh
$ curl -X POST http://localhost:5000 -d '{"jsonrpc": "2.0", "method": "ping", "id": 1}'
{"jsonrpc": "2.0", "result": "pong", "id": 1}
```

`serve` starts a basic development server. Do not use it in a production deployment. Use
a production WSGI server instead, with jsonrpcserver's [dispatch](dispatch) function.
70 changes: 70 additions & 0 deletions docs/methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Methods

Methods are functions that can be called by a JSON-RPC request.

## Writing methods

To write a method, decorate a function with `@method`:

```python
from jsonrpcserver import method, Error, Ok, Result

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

If you don't need to respond with any value simply `return Ok()`.

## Responses

Methods return either `Ok` or `Error`. These are the [JSON-RPC response
objects](https://www.jsonrpc.org/specification#response_object) (excluding the
`jsonrpc` and `id` parts). `Error` takes a code, message, and optionally
'data'.

```python
@method
def test() -> Result:
return Error(1, "There was a problem")
```

Alternatively, raise a `JsonRpcError`, which takes the same arguments as `Error`.

## Parameters

Methods can accept arguments.

```python
@method
def hello(name: str) -> Result:
return Ok("Hello " + name)
```

Testing it:

```sh
$ curl -X POST http://localhost:5000 -d '{"jsonrpc": "2.0", "method": "hello", "params": ["Beau"], "id": 1}'
{"jsonrpc": "2.0", "result": "Hello Beau", "id": 1}
```

## Invalid params

A common error response is *invalid params*.
The JSON-RPC error code for this is **-32602**. A shortcut, *InvalidParams*, is
included so you don't need to remember that.

```python
from jsonrpcserver import dispatch, method, InvalidParams, Ok, Result

@method
def within_range(num: int) -> Result:
if num not in range(1, 5):
return InvalidParams("Value must be 1-5")
return Ok()
```

This is the same as saying
```python
return Error(-32602, "Invalid params", "Value must be 1-5")
```
18 changes: 5 additions & 13 deletions jsonrpcserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,27 @@
http.server module.
"""

import logging
from http.server import BaseHTTPRequestHandler, HTTPServer

from .main import dispatch


class RequestHandler(BaseHTTPRequestHandler):
"""Handle HTTP requests"""

def do_POST(self) -> None: # pylint: disable=invalid-name
"""Handle POST request"""
response = dispatch(
self.rfile.read(int(str(self.headers["Content-Length"]))).decode()
)
request = self.rfile.read(int(str(self.headers["Content-Length"]))).decode()
response = dispatch(request)
if response is not None:
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(str(response).encode())
self.wfile.write(response.encode())


def serve(name: str = "", port: int = 5000) -> None:
"""A simple function to serve HTTP requests"""
logging.info(" * Listening on port %s", port)
httpd = HTTPServer((name, port), RequestHandler)
try:
httpd = HTTPServer((name, port), RequestHandler)
httpd.serve_forever()
except KeyboardInterrupt:
pass
except Exception:
finally:
httpd.shutdown()
raise
Binary file added logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
markdown_extensions:
- pymdownx.highlight:
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.details
- pymdownx.superfences
- pymdownx.mark
nav:
- Home: 'index.md'
- 'methods.md'
- 'dispatch.md'
- 'async.md'
- 'faq.md'
- 'examples.md'
repo_name: jsonrpcserver
repo_url: https://github.com/explodinglabs/jsonrpcserver
site_author: Exploding Labs
site_description: Welcome to the documentation for Jsonrcpcserver.
site_name: Jsonrpcserver
site_url: https://www.jsonrpcserver.com/
theme:
features:
- content.code.copy
- navigation.footer
- navigation.tabs
- toc.integrate
name: material
palette:
# Palette toggle for automatic mode
- media: "(prefers-color-scheme)"
toggle:
icon: material/brightness-auto
name: Switch to light mode
# Palette toggle for light mode
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/brightness-7
name: Switch to dark mode
# Palette toggle for dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/brightness-4
name: Switch to system preference
extra:
version:
provider: mike
Loading