jsonrpc-base

Pure python standard library JSONRPC data interface
git clone git://git.defalsify.org/python-jsonrpc-base.git
Log | Files | Refs | LICENSE

commit 6082d05093983f118f265e88917e210a64f96aee
Author: nolash <dev@holbrook.no>
Date:   Fri,  9 Apr 2021 11:06:08 +0200

Initial commit

Diffstat:
A.gitignore | 2++
Ajsonrpc_base/base.py | 4++++
Ajsonrpc_base/error.py | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ajsonrpc_base/interface.py | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asetup.cfg | 25+++++++++++++++++++++++++
Asetup.py | 3+++
Asetup.pye | 16++++++++++++++++
Atests/test_error.py | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/test_request.py | 39+++++++++++++++++++++++++++++++++++++++
9 files changed, 315 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +*.pyc diff --git a/jsonrpc_base/base.py b/jsonrpc_base/base.py @@ -0,0 +1,4 @@ +class JSONRPCBase: + major_version = 2 + minor_version = 0 + version_string = '2.0' diff --git a/jsonrpc_base/error.py b/jsonrpc_base/error.py @@ -0,0 +1,86 @@ +# local imports +from .base import JSONRPCBase + +class JSONRPCException(Exception, JSONRPCBase): + message = 'Unknown' + + def __init__(self, v): + context_v = '{} error'.format(self.message) + if v != None: + context_v += ': ' + v + + super(JSONRPCException, self).__init__(context_v) + + +class JSONRPCCustomException(JSONRPCException): + code = -32000 + message = 'Server' + + +class JSONRPCParseError(JSONRPCException): + code = -32700 + message = 'Parse' + + +class JSONRPCInvalidRequestError(JSONRPCException): + code = -32600 + message = 'Invalid request' + + +class JSONRPCMethodNotFoundError(JSONRPCException): + code = -32601 + message = 'Method not found' + + +class JSONRPCInvalidParametersError(JSONRPCException): + code = -32602 + message = 'Invalid parameters' + + +class JSONRPCInternalError(JSONRPCException): + code = -32603 + message = 'Internal' + + +class JSONRPCUnhandledErrorException(KeyError): + pass + + +class JSONRPCErrors: + reserved_max = -31999 + reserved_min = -32768 + local_max = -32000 + local_min = -32099 + + translations = { + -32700: JSONRPCParseError, + -32600: JSONRPCInvalidRequestError, + -32601: JSONRPCMethodNotFoundError, + -32602: JSONRPCInvalidParametersError, + -32603: JSONRPCInternalError, + } + + @classmethod + def add(self, code, exception_object): + if code < self.local_min or code > self.local_max: + raise ValueError('code must be in range <{},{}>'.format(self.local_min, self.local_max)) + exc = self.translations.get(code) + if exc != None: + raise ValueError('code already registered with {}'.format(exc)) + + if not issubclass(exception_object, JSONRPCCustomException): + raise ValueError('exception object must be a subclass of jsonrpc_base.error.JSONRPCCustomException') + + self.translations[code] = exception_object + + + @classmethod + def get(self, code, v=None): + e = self.translations.get(code) + if e == None: + raise JSONRPCUnhandledErrorException(code) + return e(v) + + +class InvalidJSONRPCError(ValueError): + pass diff --git a/jsonrpc_base/interface.py b/jsonrpc_base/interface.py @@ -0,0 +1,68 @@ +# standard imports +import uuid + +# local imports +from .base import JSONRPCBase +from .error import ( + JSONRPCErrors, + ) + + +class DefaultErrorParser: + + def translate(self, error): + code = error['error']['code'] + message = error['error']['message'] + if type(code).__name__ != 'int': + raise ValueError('error code is not int by {} in error {}'.format(type(code), error)) + + exc = None + try: + exc = JSONRPCErrors.get(code, message) + except KeyError: + return JSONRPCUndefinedError(code, message) + + +def jsonrpc_template(): + return { + 'jsonrpc': JSONRPCBase.version_string, + 'id': str(uuid.uuid4()), + 'method': None, + 'params': [], + } + + +def jsonrpc_request(method): + req = jsonrpc_template() + req['method'] = method + return req + + +def jsonrpc_result(o, ep): + if o.get('error') != None: + raise ep.translate(o) + return o['result'] + + +def jsonrpc_response(request_id, result): + return { + 'jsonrpc': JSONRPCBase.version_string, + 'id': request_id, + 'result': result, + } + + +def jsonrpc_error(request_id, code, message=None): + e = JSONRPCErrors.get(code, message) + return { + 'jsonrpc': JSONRPCBase.version_string, + 'id': request_id, + 'error': { + 'code': code, + 'message': str(e), + }, + } + + +def jsonrpc_is_response_to(request, response): + return request['id'] == response['id'] diff --git a/setup.cfg b/setup.cfg @@ -0,0 +1,25 @@ +[metadata] +name = jsonrpc_base +version = 0.0.1a1 +description = Pure python standard library JSONRPC data interface +author = Louis Holbrook +author_email = dev@holbrook.no +url = https://gitlab.com/nolash/jsonrpc_base +keywords = + jsonrpc + rpc +classifiers = + Programming Language :: Python :: 3 + Operating System :: OS Independent + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) + Topic :: Software Development :: Libraries +license = GPL3 +licence_files = + LICENSE.txt + +[options] +python_requires = >= 3.6 +packages = + jsonrpc_base diff --git a/setup.py b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/setup.pye b/setup.pye @@ -0,0 +1,16 @@ +from setuptools import setup +import configparser +import os + +requirements = [] +f = open('requirements.txt', 'r') +while True: + l = f.readline() + if l == '': + break + requirements.append(l.rstrip()) +f.close() + +setup( + install_requires=requirements, + ) diff --git a/tests/test_error.py b/tests/test_error.py @@ -0,0 +1,72 @@ +# standard imports +import unittest + +# local imports +from jsonrpc_base.error import ( + JSONRPCException, + JSONRPCCustomException, + JSONRPCErrors, + JSONRPCParseError, + ) +from jsonrpc_base.interface import ( + jsonrpc_error, + DefaultErrorParser, + ) + +class WrongError(Exception): + pass + + +class RightError(JSONRPCCustomException): + message = 'Right' + code = -32001 + + +class TestError(unittest.TestCase): + + def test_base(self): + e = JSONRPCException('foo') + self.assertEqual(str(e), 'Unknown error: foo') + + + def test_error_by_code(self): + e = JSONRPCErrors.get(-32700) + self.assertTrue(isinstance(e, JSONRPCParseError)) + + + def test_custom_error(self): + with self.assertRaises(KeyError): + e = JSONRPCErrors.get(-1000) + + with self.assertRaises(ValueError): + JSONRPCErrors.add(-32000, WrongError) + + with self.assertRaises(ValueError): + JSONRPCErrors.add(JSONRPCErrors.local_min - 1, RightError) + + with self.assertRaises(ValueError): + JSONRPCErrors.add(JSONRPCErrors.local_max + 1, RightError) + + JSONRPCErrors.add(-32000, RightError) + e_retrieved = JSONRPCErrors.get(-32000, 'foo') + self.assertEqual(type(RightError(None)), type(e_retrieved)) + self.assertEqual(str(e_retrieved), 'Right error: foo') + + + def test_error_interface(self): + uu = 'foo' + e = jsonrpc_error(uu, -32700) + self.assertEqual(e['error']['code'], -32700) + self.assertEqual(e['error']['message'], 'Parse error') + + + def test_default_error_translate(self): + uu = 'foo' + p = DefaultErrorParser() + e = jsonrpc_error(uu, -32700) + o = p.translate(e) + print('e {}'.format(o)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_request.py b/tests/test_request.py @@ -0,0 +1,39 @@ +# standard imports +import unittest + +# local imports +from jsonrpc_base.base import JSONRPCBase +from jsonrpc_base.interface import ( + jsonrpc_request, + jsonrpc_response, + jsonrpc_result, + jsonrpc_is_response_to, + ) + + +class TestRequest(unittest.TestCase): + + def test_request(self): + req = jsonrpc_request('foo_barBaz') + self.assertEqual(req['jsonrpc'], JSONRPCBase.version_string) + self.assertEqual(type(req['id']).__name__, 'str') + self.assertEqual(req['method'], 'foo_barBaz') + self.assertEqual(len(req['params']), 0) + + + def test_response(self): + res = jsonrpc_response('foo', 42) + self.assertEqual(res['id'], 'foo') + self.assertEqual(res['result'], 42) + r = jsonrpc_result(res, None) + self.assertEqual(r, 42) + + + def test_response_compare(self): + req = jsonrpc_request('foo_barBaz') + res = jsonrpc_response(req['id'], 42) + self.assertTrue(jsonrpc_is_response_to(req, res)) + + +if __name__ == '__main__': + unittest.main()