diff --git a/README.md b/README.md index caa0aa5..42618e8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ jsonrpcserver ![PyPI](https://img.shields.io/pypi/v/jsonrpcserver.svg) @@ -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: diff --git a/docs/async.md b/docs/async.md new file mode 100644 index 0000000..5954d07 --- /dev/null +++ b/docs/async.md @@ -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. +``` diff --git a/docs/dispatch.md b/docs/dispatch.md new file mode 100644 index 0000000..719ea9c --- /dev/null +++ b/docs/dispatch.md @@ -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) +``` diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..98bf4e7 --- /dev/null +++ b/docs/faq.md @@ -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`. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3612046 --- /dev/null +++ b/docs/index.md @@ -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. diff --git a/docs/methods.md b/docs/methods.md new file mode 100644 index 0000000..28457f8 --- /dev/null +++ b/docs/methods.md @@ -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") +``` diff --git a/jsonrpcserver/server.py b/jsonrpcserver/server.py index e783082..d0c54fb 100644 --- a/jsonrpcserver/server.py +++ b/jsonrpcserver/server.py @@ -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 diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..d7024dc Binary files /dev/null and b/logo.png differ diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..57eb2b9 --- /dev/null +++ b/mkdocs.yml @@ -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