Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0f1fd29
Replace Oslash with Returns
bcb May 14, 2022
c1426f6
Fix github action
bcb May 14, 2022
d806553
Fix github action
bcb May 14, 2022
0ede79e
Add comment to docs
bcb May 17, 2022
9451357
Merge branch 'use-returns' into prepare-v6
bcb May 18, 2022
a854670
Prepare version 6.0.0
bcb May 18, 2022
e9c2da2
Merge branch 'main' into prepare-v6
bcb May 19, 2022
8808a43
Adjust docstring
bcb May 20, 2022
03f80d0
Remove errant comma
bcb May 24, 2022
173bbca
Merge branch 'main' into release/6.0.0
bcb Sep 21, 2022
609b990
Adjust github workflow
bcb Sep 21, 2022
bae441c
Upgrade mypy in github workflow
bcb Sep 21, 2022
866a4e2
Enable all Pylint errors
bcb Oct 10, 2022
e74da97
Merge branch 'main' into release/6.0.0
bcb Oct 10, 2022
f6294ee
Fix not re-exported error
bcb Oct 10, 2022
ab30150
Upgrade pylint
bcb Nov 9, 2022
4f510b1
Pylint fixes
bcb Nov 9, 2022
73ba6a3
Update changelog
bcb Nov 9, 2022
0e67522
Fix some tests
bcb Nov 16, 2022
f4e8761
Fix parameters
bcb Feb 26, 2023
558215b
Fixes to satisfy ruff
bcb Feb 26, 2023
0a88d8f
Replace pylint with ruff
bcb Mar 3, 2023
65f798c
Adjustments to satisfy ruff and mypy
bcb Mar 3, 2023
843fdcd
Fix a repr
bcb May 10, 2023
ae90cdf
Use ruff in github actions
bcb May 10, 2023
6db0f90
Fix type error
bcb May 10, 2023
e7287b5
Upgrade ruff
bcb May 10, 2023
75cee0e
Replace setup.py with pyproject.toml
bcb May 24, 2023
9a1fe9f
Always stop the server when exiting serve()
bcb May 31, 2023
ffe8374
Replace black and isort with ruff (#278)
bcb Jul 29, 2024
be34675
Move documentation to Github wiki (#280)
bcb Jul 31, 2024
389959d
Update readme (#281)
bcb Jul 31, 2024
8c23c46
Replace readthedocs with mkdocs (#282)
bcb Aug 16, 2024
1d9153e
Remove pylint pragmas (#283)
bcb Aug 17, 2024
6efc172
Move request-schema.json to a .py file (#284)
bcb Aug 28, 2024
5c745ce
Add jsonschema dependency
bcb Nov 14, 2024
c2b807e
Add license badge
bcb Mar 22, 2025
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
Prev Previous commit
Next Next commit
Fix parameters
  • Loading branch information
bcb committed Feb 26, 2023
commit f4e8761c7673f80d198b3f4dd4c5df2289ca0c15
26 changes: 7 additions & 19 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,14 @@ repos:
- id: black
args: [--diff, --check]

- repo: https://github.com/PyCQA/pylint
rev: v2.15.5
- repo: local
hooks:
- id: pylint
stages: [manual]
additional_dependencies:
- returns<1
- aiohttp<4
- aiozmq<1
- django<5
- fastapi<1
- flask<3
- flask-socketio<5.3.1
- jsonschema<5
- pytest
- pyzmq
- sanic
- tornado<7
- uvicorn<1
- websockets<11
- id: pylint
name: pylint
entry: pylint
language: system
types: [python]
require_serial: true

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.971
Expand Down
32 changes: 24 additions & 8 deletions docs/dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ and gives a JSON-RPC response.

## Optional parameters

The `dispatch` function 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
Expand Down Expand Up @@ -43,23 +46,36 @@ def greet(context, name):

### deserializer

A function that parses the request string. Default is `json.loads`.
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)
```

### validator

A function that validates the request once the json has been parsed. The
function should raise an exception (any exception) if the request doesn't match
the JSON-RPC spec. Default is `default_validator` which validates the request
against a schema.
2 changes: 1 addition & 1 deletion examples/fastapi_server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""FastAPI server"""
from fastapi import FastAPI, Request, Response
import uvicorn # type: ignore
import uvicorn
from jsonrpcserver import dispatch, method, Ok, Result

app = FastAPI()
Expand Down
4 changes: 2 additions & 2 deletions jsonrpcserver/async_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .async_dispatcher import dispatch_to_response_pure
from .async_methods import Methods, global_methods
from .dispatcher import Deserialized
from .main import default_validator, default_deserializer
from .main import default_jsonrpc_validator, default_deserializer
from .response import Response, to_serializable
from .sentinels import NOCONTEXT
from .utils import identity
Expand All @@ -20,7 +20,7 @@ async def dispatch_to_response(
*,
context: Any = NOCONTEXT,
deserializer: Callable[[str], Deserialized] = default_deserializer,
validator: Callable[[Deserialized], Deserialized] = default_validator,
validator: Callable[[Deserialized], Deserialized] = default_jsonrpc_validator,
post_process: Callable[[Response], Any] = identity,
) -> Union[Response, Iterable[Response], None]:
return await dispatch_to_response_pure(
Expand Down
45 changes: 29 additions & 16 deletions jsonrpcserver/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .sentinels import NOCONTEXT, NOID
from .utils import compose, make_list

ArgsValidator = Callable[[Any, Request, Method], Result[Method, ErrorResult]]
Deserialized = Union[Dict[str, Any], List[Dict[str, Any]]]

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -120,7 +121,9 @@ def validate_result(result: Result[SuccessResult, ErrorResult]) -> None:


def call(
request: Request, context: Any, method: Method
request: Request,
context: Any,
method: Method,
) -> Result[SuccessResult, ErrorResult]:
"""Call the method.

Expand All @@ -146,7 +149,9 @@ def call(


def validate_args(
request: Request, context: Any, func: Method
request: Request,
context: Any,
func: Method,
) -> Result[Method, ErrorResult]:
"""Ensure the method can be called with the arguments given.

Expand All @@ -171,7 +176,10 @@ def get_method(methods: Methods, method_name: str) -> Result[Method, ErrorResult


def dispatch_request(
methods: Methods, context: Any, request: Request
args_validator: ArgsValidator,
methods: Methods,
context: Any,
request: Request,
) -> Tuple[Request, Result[SuccessResult, ErrorResult]]:
"""Get the method, validates the arguments and calls the method.

Expand All @@ -182,7 +190,7 @@ def dispatch_request(
return (
request,
get_method(methods, request.method)
.bind(partial(validate_args, request, context))
.bind(partial(args_validator, request, context))
.bind(partial(call, request, context)),
)

Expand All @@ -203,9 +211,10 @@ def not_notification(request_result: Any) -> bool:


def dispatch_deserialized(
args_validator: ArgsValidator,
post_process: Callable[[Response], Response],
methods: Methods,
context: Any,
post_process: Callable[[Response], Response],
deserialized: Deserialized,
) -> Union[Response, List[Response], None]:
"""This is simply continuing the pipeline from dispatch_to_response_pure. It exists
Expand All @@ -216,15 +225,17 @@ def dispatch_deserialized(
applied to the Response(s).
"""
results = map(
compose(partial(dispatch_request, methods, context), create_request),
compose(
partial(dispatch_request, args_validator, methods, context), create_request
),
make_list(deserialized),
)
responses = starmap(to_response, filter(not_notification, results))
return extract_list(isinstance(deserialized, list), map(post_process, responses))


def validate_request(
validator: Callable[[Deserialized], Deserialized], request: Deserialized
jsonrpc_validator: Callable[[Deserialized], Deserialized], request: Deserialized
) -> Result[Deserialized, ErrorResponse]:
"""Validate the request against a JSON-RPC schema.

Expand All @@ -233,9 +244,9 @@ def validate_request(
Returns: Either the same request passed in or an Invalid request response.
"""
try:
validator(request)
jsonrpc_validator(request)
# Since the validator is unknown, the specific exception that will be raised is also
# unknown. Any exception raised we assume the request is invalid and return an
# unknown. Any exception raised we assume the request is invalid and return an
# "invalid request" response.
except Exception: # pylint: disable=broad-except
return Failure(InvalidRequestResponse("The request failed schema validation"))
Expand All @@ -259,29 +270,31 @@ def deserialize_request(


def dispatch_to_response_pure(
*,
args_validator: ArgsValidator,
deserializer: Callable[[str], Deserialized],
validator: Callable[[Deserialized], Deserialized],
jsonrpc_validator: Callable[[Deserialized], Deserialized],
post_process: Callable[[Response], Response],
methods: Methods,
context: Any,
post_process: Callable[[Response], Response],
request: str,
) -> Union[Response, List[Response], None]:
"""A function from JSON-RPC request string to Response namedtuple(s), (yet to be
serialized to json).

Returns: A single Response, a list of Responses, or None. None is given for
notifications or batches of notifications, to indicate that we should not
respond.
notifications or batches of notifications, to indicate that we should
not respond.
"""
try:
result = deserialize_request(deserializer, request).bind(
partial(validate_request, validator)
partial(validate_request, jsonrpc_validator)
)
return (
post_process(result)
if isinstance(result, Failure)
else dispatch_deserialized(methods, context, post_process, result.unwrap())
else dispatch_deserialized(
args_validator, post_process, methods, context, result.unwrap()
)
)
except Exception as exc: # pylint: disable=broad-except
# There was an error with the jsonrpcserver library.
Expand Down
43 changes: 27 additions & 16 deletions jsonrpcserver/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,38 @@

from jsonschema.validators import validator_for # type: ignore

from .dispatcher import dispatch_to_response_pure, Deserialized
from .dispatcher import (
ArgsValidator,
Deserialized,
dispatch_to_response_pure,
validate_args,
)
from .methods import Methods, global_methods
from .response import Response, to_dict
from .sentinels import NOCONTEXT
from .utils import identity


default_args_validator = validate_args
default_deserializer = json.loads

# Prepare the jsonschema validator. This is global so it loads only once, not every
# time dispatch is called.
# Prepare the jsonschema validator. This is global so it loads only once, not every time
# dispatch is called.
schema = json.loads(read_text(__package__, "request-schema.json"))
klass = validator_for(schema)
klass.check_schema(schema)
default_validator = klass(schema).validate
default_jsonrpc_validator = klass(schema).validate


def dispatch_to_response(
request: str,
methods: Optional[Methods] = None,
*,
methods: Methods = global_methods,
context: Any = NOCONTEXT,
args_validator: ArgsValidator = default_args_validator,
deserializer: Callable[[str], Deserialized] = json.loads,
validator: Callable[[Deserialized], Deserialized] = default_validator,
jsonrpc_validator: Callable[
[Deserialized], Deserialized
] = default_jsonrpc_validator,
post_process: Callable[[Response], Any] = identity,
) -> Union[Response, List[Response], None]:
"""Takes a JSON-RPC request string and dispatches it to method(s), giving Response
Expand All @@ -54,9 +62,11 @@ def dispatch_to_response(
populated with the @method decorator.
context: If given, will be passed as the first argument to methods.
deserializer: Function that deserializes the request string.
validator: Function that validates the JSON-RPC request. The function should
raise an exception if the request is invalid. To disable validation, pass
lambda _: None.
args_validator: Function that validates that the parameters in the request match
the Python function being called.
jsonrpc_validator: Function that validates the JSON-RPC request. The function
should raise an exception if the request is invalid. To disable validation,
pass lambda _: None.
post_process: Function that will be applied to Responses.

Returns:
Expand All @@ -67,12 +77,13 @@ def dispatch_to_response(
'{"jsonrpc": "2.0", "result": "pong", "id": 1}'
"""
return dispatch_to_response_pure(
deserializer=deserializer,
validator=validator,
post_process=post_process,
context=context,
methods=global_methods if methods is None else methods,
request=request,
args_validator,
deserializer,
jsonrpc_validator,
post_process,
methods,
context,
request,
)


Expand Down
6 changes: 3 additions & 3 deletions tests/test_async_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
dispatch_request,
dispatch_to_response_pure,
)
from jsonrpcserver.main import default_deserializer, default_validator
from jsonrpcserver.main import default_deserializer, default_jsonrpc_validator
from jsonrpcserver.codes import ERROR_INTERNAL_ERROR, ERROR_SERVER_ERROR
from jsonrpcserver.exceptions import JsonRpcError
from jsonrpcserver.request import Request
Expand Down Expand Up @@ -76,7 +76,7 @@ async def test_dispatch_deserialized() -> None:
async def test_dispatch_to_response_pure_success() -> None:
assert await dispatch_to_response_pure(
deserializer=default_deserializer,
validator=default_validator,
validator=default_jsonrpc_validator,
post_process=identity,
context=NOCONTEXT,
methods={"ping": ping},
Expand All @@ -92,7 +92,7 @@ async def ping() -> Result:

assert await dispatch_to_response_pure(
deserializer=default_deserializer,
validator=default_validator,
validator=default_jsonrpc_validator,
post_process=identity,
context=NOCONTEXT,
methods={"ping": ping},
Expand Down
Loading