├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app.py ├── requirements-dev.txt ├── requirements.txt ├── setup.py ├── starlette_jsonrpc ├── __init__.py ├── constants.py ├── dispatcher.py ├── endpoint.py ├── exceptions.py └── schemas.py └── tests ├── __init__.py ├── test_dispatcher.py ├── test_requests.py └── test_validating.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # IDE 107 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: required 4 | 5 | dist: xenial 6 | 7 | python: 8 | - "3.6" 9 | - "3.7" 10 | 11 | install: 12 | - pip install -r requirements.txt 13 | - pip install -r requirements-dev.txt 14 | 15 | script: 16 | - pytest 17 | - pytest --cov=starlette_jsonrpc --cov=tests 18 | 19 | after_success: 20 | - codecov -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | *** 4 | ### ver 0.1.0 - 18.04.2019 5 | Github: [Release](https://github.com/kdebowski/starlette-jsonrpc/releases/tag/0.1.0) 6 | Pypi: [Package](https://pypi.org/project/starlette-jsonrpc/0.1.0/) 7 | 8 | Added: 9 | * Added installation guide to readme 10 | 11 | Changed: 12 | * Reformat code with Black formaterr 13 | * Changed namings 14 | * Methods should return actual result, not {"result": "here actual result"} obiect 15 | 16 | Removed 17 | * Removed todo section from readme 18 | 19 | *** 20 | ### ver 0.0.0 - 9.04.2019 21 | Github: [Release](https://github.com/kdebowski/starlette-jsonrpc/releases/tag/0.0.0) 22 | Pypi: [Package](https://pypi.org/project/starlette-jsonrpc/0.0.0/) 23 | 24 | Initial release -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contibuting to starlette-jsonrpc 2 | 3 | Everyone is welcome to contribute to the project. It is recommended to discuss proposed changes in separate issue. 4 | 5 | Feel free to submit issues and take part in discussions. 6 | 7 | ### Pull request process: 8 | 9 | In general, project follows the "fork-and-pull" workflow. 10 | 11 | 1. Fork the repo on GitHub 12 | 2. Clone the project to your own machine 13 | 3. Commit changes to your own branch 14 | 4. Push your work back up to your fork 15 | 5. Submit a Pull request so that your changes can be reviewed. 16 | 17 | NOTE: Be sure to merge the latest from "upstream" before making a pull request! 18 | 19 | ### Additional informations: 20 | 21 | * Ensure any install or build dependencies are removed before committing code. 22 | * Ensure all unit tests pass. 23 | * Add proper documentation concerning your changes if required. 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kamil Dębowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # starlette-jsonrpc 2 | 3 | [![Build Status](https://travis-ci.com/kdebowski/starlette-jsonrpc.svg?token=JXg8SCx8Y9Ybz183mTgo&branch=master)](https://travis-ci.com/kdebowski/starlette-jsonrpc) 4 | [![codecov](https://codecov.io/gh/kdebowski/starlette-jsonrpc/branch/master/graph/badge.svg?token=3DkWshhv8x)](https://codecov.io/gh/kdebowski/starlette-jsonrpc) 5 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 6 | 7 | ## Installation 8 | 9 | ```bash 10 | pip install starlette-jsonrpc 11 | ``` 12 | 13 | 14 | ## Examples 15 | 16 | Code: 17 | ```python 18 | import uvicorn 19 | from starlette.applications import Starlette 20 | 21 | from starlette_jsonrpc import dispatcher 22 | from starlette_jsonrpc.endpoint import JSONRPCEndpoint 23 | 24 | 25 | app = Starlette() 26 | 27 | 28 | @dispatcher.add_method 29 | async def subtract(params): 30 | return params["x"] - params["y"] 31 | 32 | 33 | @dispatcher.add_method(name="SubtractMethod") 34 | async def seconds_subtract(params): 35 | return params["x"] - params["y"] 36 | 37 | 38 | @dispatcher.add_method 39 | async def subtract_positional(x, y): 40 | return x - y 41 | 42 | 43 | app.mount("/api", JSONRPCEndpoint) 44 | 45 | if __name__ == "__main__": 46 | uvicorn.run(app) 47 | ``` 48 | 49 | Example of requests: 50 | 51 | ```json 52 | { 53 | "jsonrpc": "2.0", 54 | "method": "subtract", 55 | "params": {"x": 42, "y": 23}, 56 | "id": "1" 57 | } 58 | ``` 59 | 60 | ```json 61 | { 62 | "jsonrpc": "2.0", 63 | "method": "SubtractMethod", 64 | "params": {"x": 42, "y": 23}, 65 | "id": "1" 66 | } 67 | ``` 68 | 69 | ```json 70 | { 71 | "jsonrpc": "2.0", 72 | "method": "subtract_positional", 73 | "params": [42, 23], 74 | "id": "1" 75 | } 76 | ``` 77 | 78 | Example of response: 79 | 80 | ```json 81 | { 82 | "jsonrpc": "2.0", 83 | "id": "1", 84 | "result": 19 85 | } 86 | ``` 87 | 88 | ## Contributing 89 | 90 | Thank you for your interest in contributing. Everyone is welcome to take part in developting this package. Please fFollow contributing guide in [CONTRIBUTING.md](https://github.com/kdebowski/starlette-jsonrpc/blob/master/CONTRIBUTING.md). 91 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from starlette.applications import Starlette 3 | 4 | from starlette_jsonrpc import dispatcher 5 | from starlette_jsonrpc.endpoint import JSONRPCEndpoint 6 | 7 | app = Starlette() 8 | 9 | 10 | @dispatcher.add_method 11 | async def subtract(params): 12 | return params["x"] - params["y"] 13 | 14 | 15 | @dispatcher.add_method(name="SubtractMethod") 16 | async def seconds_subtract(params): 17 | return params["x"] - params["y"] 18 | 19 | 20 | app.mount("/api", JSONRPCEndpoint) 21 | 22 | 23 | if __name__ == "__main__": 24 | uvicorn.run(app) 25 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | black==19.3b0 4 | codecov==2.0.15 5 | pytest==4.4.0 6 | pytest-cov==2.6.1 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.21.0 2 | starlette==0.11.4 3 | typesystem==0.2.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import setuptools 3 | 4 | 5 | dir_path = os.path.dirname(os.path.realpath(__file__)) 6 | 7 | 8 | def read(fname): 9 | with open(fname, "r") as f: 10 | return f.read() 11 | 12 | 13 | install_requires = [ 14 | l 15 | for l in read(os.path.join(dir_path, "requirements.txt")).splitlines() 16 | if l and not l.startswith("#") 17 | ] 18 | 19 | 20 | setuptools.setup( 21 | name="starlette-jsonrpc", 22 | version="0.2.0", 23 | author="Kamil Dębowski", 24 | author_email="poczta@kdebowski.pl", 25 | description="JSON-RPC implementation for Starlette framework", 26 | long_description=read("README.md"), 27 | long_description_content_type="text/markdown", 28 | url="https://github.com/kdebowski/starlette-jsonrpc", 29 | packages=setuptools.find_packages(), 30 | classifiers=[ 31 | "Development Status :: 3 - Alpha", 32 | "Programming Language :: Python :: 3", 33 | "License :: OSI Approved :: MIT License", 34 | "Operating System :: OS Independent", 35 | "Intended Audience :: Developers", 36 | ], 37 | install_requires=install_requires, 38 | include_package_data=True, 39 | ) 40 | -------------------------------------------------------------------------------- /starlette_jsonrpc/__init__.py: -------------------------------------------------------------------------------- 1 | from starlette_jsonrpc.dispatcher import Dispatcher 2 | 3 | 4 | dispatcher = Dispatcher() 5 | -------------------------------------------------------------------------------- /starlette_jsonrpc/constants.py: -------------------------------------------------------------------------------- 1 | JSONRPC_VERSION = "2.0" 2 | -------------------------------------------------------------------------------- /starlette_jsonrpc/dispatcher.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | 4 | class Dispatcher: 5 | def __init__(self): 6 | self.methods_map = {} 7 | 8 | def add_method(self, method: str = None, name: str = None): 9 | 10 | if name and not method: 11 | return functools.partial(self.add_method, name=name) 12 | 13 | self.methods_map[name or method.__name__] = method 14 | return method 15 | -------------------------------------------------------------------------------- /starlette_jsonrpc/endpoint.py: -------------------------------------------------------------------------------- 1 | from json import JSONDecodeError 2 | 3 | from starlette.endpoints import HTTPEndpoint 4 | from starlette.requests import Request 5 | from starlette.responses import JSONResponse 6 | from starlette.responses import Response 7 | 8 | from starlette_jsonrpc import dispatcher 9 | from starlette_jsonrpc.constants import JSONRPC_VERSION 10 | from starlette_jsonrpc.exceptions import JSONRPCException 11 | from starlette_jsonrpc.exceptions import JSONRPCInvalidParamsException 12 | from starlette_jsonrpc.exceptions import JSONRPCInvalidRequestException 13 | from starlette_jsonrpc.exceptions import JSONRPCMethodNotFoundException 14 | from starlette_jsonrpc.exceptions import JSONRPCParseErrorException 15 | from starlette_jsonrpc.schemas import JSONRPCErrorResponse 16 | from starlette_jsonrpc.schemas import JSONRPCRequest 17 | from starlette_jsonrpc.schemas import JSONRPCResponse 18 | 19 | 20 | class JSONRPCEndpoint(HTTPEndpoint): 21 | async def post(self, request: Request) -> Response: 22 | try: 23 | response = await self._get_response(request) 24 | except JSONRPCInvalidParamsException as exc: 25 | return self._get_exception_response(exc) 26 | except JSONRPCMethodNotFoundException as exc: 27 | return self._get_exception_response(exc) 28 | except JSONRPCInvalidRequestException as exc: 29 | return self._get_exception_response(exc) 30 | except JSONRPCParseErrorException as exc: 31 | return self._get_exception_response(exc) 32 | 33 | return JSONResponse(response) 34 | 35 | async def _get_response(self, request: Request) -> dict: 36 | try: 37 | req = await request.json() 38 | except JSONDecodeError: 39 | raise JSONRPCParseErrorException() 40 | except Exception: 41 | raise JSONRPCInvalidRequestException() 42 | 43 | if not self._valid_request(req): 44 | raise JSONRPCInvalidRequestException() 45 | 46 | if not req: 47 | raise JSONRPCInvalidRequestException() 48 | 49 | if self._is_notification(req): 50 | return {} 51 | 52 | data, errors = JSONRPCRequest.validate_or_error(req) 53 | id = req.get("id") 54 | 55 | if errors: 56 | raise JSONRPCInvalidParamsException(id, errors) 57 | 58 | method = data.get("method") 59 | func = dispatcher.methods_map.get(method) 60 | 61 | if not func: 62 | raise JSONRPCMethodNotFoundException(id) 63 | 64 | params = data.get("params") 65 | 66 | result = await self._get_result(params, func, id) 67 | 68 | response = JSONRPCResponse.validate( 69 | {"id": id, "jsonrpc": JSONRPC_VERSION, "result": result} 70 | ) 71 | return dict(response) 72 | 73 | @staticmethod 74 | def _valid_request(req) -> bool: 75 | if isinstance(req, dict): 76 | return True 77 | if isinstance(req, list): 78 | if all([isinstance(elem, dict) for elem in req]): 79 | return True 80 | return False 81 | 82 | @staticmethod 83 | def _is_notification(req: dict) -> bool: 84 | if all(k in req for k in ("jsonrpc", "method", "params")) and not "id" in req: 85 | return True 86 | return False 87 | 88 | @staticmethod 89 | async def _get_result(params, func, id: str = None) -> dict: 90 | if isinstance(params, list): 91 | try: 92 | return await func(*params) 93 | except TypeError as e: 94 | errors = {"params": f"{e}"} 95 | raise JSONRPCInvalidParamsException(id, errors) 96 | elif isinstance(params, dict): 97 | try: 98 | return await func(params) 99 | except KeyError as e: 100 | errors = {"params": f"Required param: {e}"} 101 | raise JSONRPCInvalidParamsException(id, errors) 102 | else: 103 | raise JSONRPCInvalidRequestException() 104 | 105 | @staticmethod 106 | def _get_exception_response(exc: JSONRPCException) -> JSONResponse: 107 | response = JSONRPCErrorResponse.validate( 108 | { 109 | "jsonrpc": "2.0", 110 | "id": str(exc.id), 111 | "error": { 112 | "code": exc.CODE, 113 | "message": exc.MESSAGE, 114 | "data": {key: value for (key, value) in exc.errors.items()}, 115 | }, 116 | } 117 | ) 118 | return JSONResponse(dict(response)) 119 | -------------------------------------------------------------------------------- /starlette_jsonrpc/exceptions.py: -------------------------------------------------------------------------------- 1 | class JSONRPCException(Exception): 2 | CODE = None 3 | MESSAGE = None 4 | 5 | def __init__(self, id: str = None, errors: dict = None) -> None: 6 | self._id = id 7 | self._errors = errors or {} 8 | 9 | @property 10 | def id(self): 11 | return self._id 12 | 13 | @property 14 | def errors(self): 15 | return self._errors 16 | 17 | 18 | class JSONRPCMethodNotFoundException(JSONRPCException): 19 | CODE = -32601 20 | MESSAGE = "Method not found." 21 | 22 | 23 | class JSONRPCInvalidParamsException(JSONRPCException): 24 | CODE = -32602 25 | MESSAGE = "Invalid params." 26 | 27 | 28 | class JSONRPCInvalidRequestException(JSONRPCException): 29 | CODE = -32600 30 | MESSAGE = "Invalid Request." 31 | 32 | 33 | class JSONRPCParseErrorException(JSONRPCException): 34 | CODE = -32700 35 | MESSAGE = "Parse error." 36 | -------------------------------------------------------------------------------- /starlette_jsonrpc/schemas.py: -------------------------------------------------------------------------------- 1 | import typesystem 2 | 3 | 4 | class ErrorSchema(typesystem.Schema): 5 | code = typesystem.Integer() 6 | message = typesystem.String() 7 | data = typesystem.Object(additional_properties=True) 8 | 9 | 10 | class JSONRPCRequest(typesystem.Schema): 11 | jsonrpc = typesystem.String(pattern="2.0", trim_whitespace=False) 12 | id = typesystem.Union( 13 | any_of=[ 14 | typesystem.String(allow_null=True, min_length=1, trim_whitespace=False), 15 | typesystem.Integer(allow_null=True), 16 | ] 17 | ) 18 | params = typesystem.Union( 19 | any_of=[typesystem.Object(additional_properties=True), typesystem.Array()] 20 | ) 21 | method = typesystem.String() 22 | 23 | 24 | class JSONRPCResponse(typesystem.Schema): 25 | jsonrpc = typesystem.String(pattern="2.0", trim_whitespace=False) 26 | id = typesystem.Union( 27 | any_of=[ 28 | typesystem.String(allow_null=True, min_length=1, trim_whitespace=False), 29 | typesystem.Integer(allow_null=True), 30 | ] 31 | ) 32 | result = typesystem.Any() 33 | 34 | 35 | class JSONRPCNotificationResponse(typesystem.Schema): 36 | jsonrpc = typesystem.String(pattern="2.0", trim_whitespace=False) 37 | method = typesystem.String() 38 | 39 | 40 | class JSONRPCErrorResponse(typesystem.Schema): 41 | jsonrpc = typesystem.String(pattern="2.0", trim_whitespace=False) 42 | id = typesystem.String(trim_whitespace=False) 43 | error = typesystem.Reference(to=ErrorSchema) 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from starlette.applications import Starlette 2 | from starlette.testclient import TestClient 3 | 4 | from starlette_jsonrpc import dispatcher 5 | from starlette_jsonrpc.endpoint import JSONRPCEndpoint 6 | 7 | 8 | app = Starlette() 9 | 10 | 11 | @dispatcher.add_method 12 | async def sum(params): 13 | return {"sum": params["x"] + params["y"]} 14 | 15 | 16 | @dispatcher.add_method 17 | async def subtract(params): 18 | return params["x"] - params["y"] 19 | 20 | 21 | @dispatcher.add_method(name="SubtractMethod") 22 | async def second_method(params): 23 | return params["x"] - params["y"] 24 | 25 | 26 | @dispatcher.add_method 27 | async def subtract_positional(x, y): 28 | return x - y 29 | 30 | 31 | @dispatcher.add_method 32 | async def get_data(params): 33 | return {"result": ["hello", 5]} 34 | 35 | 36 | app.mount("/api", JSONRPCEndpoint) 37 | 38 | client = TestClient(app) 39 | -------------------------------------------------------------------------------- /tests/test_dispatcher.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | from starlette_jsonrpc import dispatcher 4 | 5 | 6 | def test_adding_methods_to_dispatcher(): 7 | func = dispatcher.methods_map.get("subtract") 8 | assert func is not None 9 | assert isinstance(func, types.FunctionType) is True 10 | 11 | func = dispatcher.methods_map.get("SubtractMethod") 12 | assert func is not None 13 | assert isinstance(func, types.FunctionType) is True 14 | 15 | func = dispatcher.methods_map.get("subtract_positional") 16 | assert func is not None 17 | assert isinstance(func, types.FunctionType) is True 18 | -------------------------------------------------------------------------------- /tests/test_requests.py: -------------------------------------------------------------------------------- 1 | from . import client 2 | 3 | 4 | def test_post_call_should_return_status_code_200(): 5 | payload = { 6 | "jsonrpc": "2.0", 7 | "method": "subtract", 8 | "params": {"x": 42, "y": 23}, 9 | "id": "1", 10 | } 11 | response = client.post("/api/", json=payload) 12 | assert response.status_code == 200 13 | 14 | 15 | def test_post_for_named_function_should_return_status_code_200(): 16 | payload = { 17 | "jsonrpc": "2.0", 18 | "method": "SubtractMethod", 19 | "params": {"x": 42, "y": 23}, 20 | "id": "1", 21 | } 22 | response = client.post("/api/", json=payload) 23 | assert response.status_code == 200 24 | 25 | 26 | def test_get_call_should_return_method_not_allowed(): 27 | response = client.get("/api/") 28 | assert response.status_code == 405 29 | 30 | 31 | def test_put_call_should_return_method_not_allowed(): 32 | payload = { 33 | "jsonrpc": "2.0", 34 | "method": "subtract", 35 | "params": {"x": 42, "y": 23}, 36 | "id": "1", 37 | } 38 | response = client.put("/api/", data=payload) 39 | assert response.status_code == 405 40 | 41 | 42 | def test_delete_call_should_return_method_not_allowed(): 43 | response = client.delete("/api/") 44 | assert response.status_code == 405 45 | 46 | 47 | def test_patch_call_should_return_method_not_allowed(): 48 | payload = { 49 | "jsonrpc": "2.0", 50 | "method": "subtract", 51 | "params": {"x": 42, "y": 23}, 52 | "id": "1", 53 | } 54 | response = client.patch("/api/", payload) 55 | assert response.status_code == 405 56 | -------------------------------------------------------------------------------- /tests/test_validating.py: -------------------------------------------------------------------------------- 1 | from . import client 2 | 3 | 4 | # JSON 5 | 6 | 7 | def test_payload_as_empty_dict(): 8 | payload = {} 9 | response = client.post("/api/", json=payload) 10 | assert response.json() == { 11 | "jsonrpc": "2.0", 12 | "id": "None", 13 | "error": {"code": -32600, "message": "Invalid Request.", "data": {}}, 14 | } 15 | 16 | 17 | def test_payload_as_empty_list(): 18 | payload = [] 19 | response = client.post("/api/", json=payload) 20 | assert response.json() == { 21 | "jsonrpc": "2.0", 22 | "id": "None", 23 | "error": {"code": -32600, "message": "Invalid Request.", "data": {}}, 24 | } 25 | 26 | 27 | def test_incorrect_payload(): 28 | payload = [1] 29 | response = client.post("/api/", json=payload) 30 | assert response.json() == { 31 | "jsonrpc": "2.0", 32 | "id": "None", 33 | "error": {"code": -32600, "message": "Invalid Request.", "data": {}}, 34 | } 35 | 36 | 37 | # PARAMS 38 | 39 | 40 | def test_positional_parameters(): 41 | payload = { 42 | "jsonrpc": "2.0", 43 | "method": "subtract_positional", 44 | "params": [42, 23], 45 | "id": "1", 46 | } 47 | response = client.post("/api/", json=payload) 48 | assert response.json() == {"jsonrpc": "2.0", "id": "1", "result": 19} 49 | 50 | 51 | def test_positional_parameters_2(): 52 | payload = { 53 | "jsonrpc": "2.0", 54 | "method": "subtract_positional", 55 | "params": [23, 42], 56 | "id": "1", 57 | } 58 | response = client.post("/api/", json=payload) 59 | assert response.json() == {"jsonrpc": "2.0", "id": "1", "result": -19} 60 | 61 | 62 | def test_named_parameters(): 63 | payload = { 64 | "jsonrpc": "2.0", 65 | "method": "SubtractMethod", 66 | "params": {"x": 42, "y": 23}, 67 | "id": "1", 68 | } 69 | response = client.post("/api/", json=payload) 70 | assert response.json() == {"jsonrpc": "2.0", "id": "1", "result": 19} 71 | 72 | 73 | def test_named_parameters_2(): 74 | payload = { 75 | "jsonrpc": "2.0", 76 | "method": "SubtractMethod", 77 | "params": {"y": 23, "x": 42}, 78 | "id": "1", 79 | } 80 | response = client.post("/api/", json=payload) 81 | assert response.json() == {"jsonrpc": "2.0", "id": "1", "result": 19} 82 | 83 | 84 | def test_named_parameters_3(): 85 | payload = { 86 | "jsonrpc": "2.0", 87 | "method": "sum", 88 | "params": {"x": 42, "y": 23}, 89 | "id": "1", 90 | } 91 | response = client.post("/api/", json=payload) 92 | assert response.json() == {"jsonrpc": "2.0", "id": "1", "result": {"sum": 65}} 93 | 94 | 95 | def test_params_not_object(): 96 | payload = {"jsonrpc": "2.0", "method": "subtract", "params": "", "id": "1"} 97 | response = client.post("/api/", json=payload) 98 | assert response.json() == { 99 | "jsonrpc": "2.0", 100 | "id": "1", 101 | "error": { 102 | "code": -32602, 103 | "message": "Invalid params.", 104 | "data": {"params": "Did not match any valid type."}, 105 | }, 106 | } 107 | 108 | 109 | def test_params_as_invalid_object(): 110 | payload = {"jsonrpc": "2.0", "method": "subtract", "params": {}, "id": "1"} 111 | response = client.post("/api/", json=payload) 112 | assert response.json() == { 113 | "jsonrpc": "2.0", 114 | "id": "1", 115 | "error": { 116 | "code": -32602, 117 | "message": "Invalid params.", 118 | "data": {"params": "Required param: 'x'"}, 119 | }, 120 | } 121 | 122 | 123 | def test_params_as_invalid_list(): 124 | payload = { 125 | "jsonrpc": "2.0", 126 | "method": "subtract_positional", 127 | "params": [1], 128 | "id": "1", 129 | } 130 | response = client.post("/api/", json=payload) 131 | assert response.json() == { 132 | "jsonrpc": "2.0", 133 | "id": "1", 134 | "error": { 135 | "code": -32602, 136 | "message": "Invalid params.", 137 | "data": { 138 | "params": "subtract_positional() missing 1 required positional argument: 'y'" 139 | }, 140 | }, 141 | } 142 | 143 | 144 | def test_without_params(): 145 | payload = {"jsonrpc": "2.0", "method": "my_method", "id": "1"} 146 | response = client.post("/api/", json=payload) 147 | assert response.status_code == 200 148 | 149 | 150 | # ID 151 | 152 | 153 | def test_id_as_integer(): 154 | payload = { 155 | "jsonrpc": "2.0", 156 | "method": "subtract", 157 | "params": {"x": 42, "y": 23}, 158 | "id": 1, 159 | } 160 | response = client.post("/api/", json=payload) 161 | assert response.json() == {"jsonrpc": "2.0", "id": 1, "result": 19} 162 | 163 | 164 | def test_id_as_string(): 165 | payload = { 166 | "jsonrpc": "2.0", 167 | "method": "subtract", 168 | "params": {"x": 42, "y": 23}, 169 | "id": "abc", 170 | } 171 | response = client.post("/api/", json=payload) 172 | assert response.json() == {"jsonrpc": "2.0", "id": "abc", "result": 19} 173 | 174 | 175 | def test_id_as_null(): 176 | payload = { 177 | "jsonrpc": "2.0", 178 | "method": "subtract", 179 | "params": {"x": 42, "y": 23}, 180 | "id": None, 181 | } 182 | response = client.post("/api/", json=payload) 183 | assert response.json() == {"jsonrpc": "2.0", "id": None, "result": 19} 184 | 185 | 186 | def test_empty_id(): 187 | payload = { 188 | "jsonrpc": "2.0", 189 | "method": "subtract", 190 | "params": {"x": 42, "y": 23}, 191 | "id": "", 192 | } 193 | response = client.post("/api/", json=payload) 194 | assert response.json() == {"jsonrpc": "2.0", "id": None, "result": 19} 195 | 196 | 197 | def test_notification(): 198 | """ 199 | Notification 200 | """ 201 | payload = {"jsonrpc": "2.0", "method": "subtract", "params": {"x": 42, "y": 23}} 202 | response = client.post("/api/", json=payload) 203 | assert response.json() == {} 204 | 205 | 206 | # JSONRPC 207 | 208 | 209 | def test_jsonrpc_as_integer(): 210 | payload = { 211 | "jsonrpc": 2, 212 | "method": "subtract", 213 | "params": {"x": 42, "y": 23}, 214 | "id": "1", 215 | } 216 | response = client.post("/api/", json=payload) 217 | assert response.json() == { 218 | "jsonrpc": "2.0", 219 | "id": "1", 220 | "error": { 221 | "code": -32602, 222 | "message": "Invalid params.", 223 | "data": {"jsonrpc": "Must be a string."}, 224 | }, 225 | } 226 | 227 | 228 | def test_empty_jsonrpc(): 229 | payload = { 230 | "jsonrpc": "", 231 | "method": "subtract", 232 | "params": {"x": 42, "y": 23}, 233 | "id": "1", 234 | } 235 | response = client.post("/api/", json=payload) 236 | assert response.json() == { 237 | "jsonrpc": "2.0", 238 | "id": "1", 239 | "error": { 240 | "code": -32602, 241 | "message": "Invalid params.", 242 | "data": {"jsonrpc": "Must not be blank."}, 243 | }, 244 | } 245 | 246 | 247 | def test_jsonrpc_wrong_value(): 248 | payload = { 249 | "jsonrpc": "3.0", 250 | "method": "subtract", 251 | "params": {"x": 42, "y": 23}, 252 | "id": "1", 253 | } 254 | response = client.post("/api/", json=payload) 255 | assert response.json() == { 256 | "jsonrpc": "2.0", 257 | "id": "1", 258 | "error": { 259 | "code": -32602, 260 | "message": "Invalid params.", 261 | "data": {"jsonrpc": "Must match the pattern /2.0/."}, 262 | }, 263 | } 264 | 265 | 266 | def test_without_jsonrpc(): 267 | payload = {"method": "subtract", "params": {"x": 42, "y": 23}, "id": "1"} 268 | response = client.post("/api/", json=payload) 269 | assert response.json() == { 270 | "jsonrpc": "2.0", 271 | "id": "1", 272 | "error": { 273 | "code": -32602, 274 | "message": "Invalid params.", 275 | "data": {"jsonrpc": "This field is required."}, 276 | }, 277 | } 278 | 279 | 280 | # METHOD 281 | 282 | 283 | def test_not_registered_method(): 284 | payload = { 285 | "jsonrpc": "2.0", 286 | "method": "non_existing_method", 287 | "params": {"x": 42, "y": 23}, 288 | "id": "1", 289 | } 290 | response = client.post("/api/", json=payload) 291 | assert response.json() == { 292 | "jsonrpc": "2.0", 293 | "id": "1", 294 | "error": {"code": -32601, "message": "Method not found.", "data": {}}, 295 | } 296 | 297 | 298 | def test_without_method(): 299 | payload = {"jsonrpc": "2.0", "params": {"x": 42, "y": 23}, "id": "1"} 300 | response = client.post("/api/", json=payload) 301 | assert response.json() == { 302 | "jsonrpc": "2.0", 303 | "id": "1", 304 | "error": { 305 | "code": -32602, 306 | "message": "Invalid params.", 307 | "data": {"method": "This field is required."}, 308 | }, 309 | } 310 | 311 | 312 | def test_with_empty_method(): 313 | payload = {"jsonrpc": "2.0", "method": "", "params": {"x": 42, "y": 23}, "id": "1"} 314 | response = client.post("/api/", json=payload) 315 | assert response.json() == { 316 | "jsonrpc": "2.0", 317 | "id": "1", 318 | "error": { 319 | "code": -32602, 320 | "message": "Invalid params.", 321 | "data": {"method": "Must not be blank."}, 322 | }, 323 | } 324 | 325 | 326 | def test_method_as_integer(): 327 | payload = {"jsonrpc": "2.0", "method": 1, "params": {"x": 42, "y": 23}, "id": "1"} 328 | response = client.post("/api/", json=payload) 329 | assert response.json() == { 330 | "jsonrpc": "2.0", 331 | "id": "1", 332 | "error": { 333 | "code": -32602, 334 | "message": "Invalid params.", 335 | "data": {"method": "Must be a string."}, 336 | }, 337 | } 338 | 339 | 340 | # def test_with_method_name_starting_with_rpc_period(): 341 | # pass 342 | --------------------------------------------------------------------------------