Package: aiohttp-rpc
aiohttp-rpcA library for a simple integration of theJSON-RPC 2.0 protocolto a Python application usingaiohttp.
The motivation is to provide a simple, fast and reliable way to integrate the JSON-RPC 2.0 protocol into your application on the server and/or client side.The library has only one dependency:aiohttp- Async http client/server frameworkTable Of ContentsInstallationpipUsageHTTP Server ExampleHTTP Client ExampleIntegrationMiddlewareWebSocketsWS Server ExampleWS Client ExampleAPI ReferenceMore examplesLicenseInstallationpippipinstallaiohttp-rpcUsageHTTP Server Examplefromaiohttpimportwebimportaiohttp_rpcdefecho(*args,**kwargs):return{'args':args,'kwargs':kwargs,}# If the function has rpc_request in arguments, then it is automatically passedasyncdefping(rpc_request):return'pong'if__name__=='__main__':aiohttp_rpc.rpc_server.add_methods([ping,echo,])app=web.Application()app.router.add_routes([web.post('/rpc',aiohttp_rpc.rpc_server.handle_http_request),])web.run_app(app,host='0.0.0.0',port=8080)HTTP Client Exampleimportaiohttp_rpcimportasyncioasyncdefrun():asyncwithaiohttp_rpc.JsonRpcClient('http://0.0.0.0:8080/rpc')asrpc:print('#1',awaitrpc.ping())print('#2',awaitrpc.echo('one','two'))print('#3',awaitrpc.call('echo',three='3'))print('#4',awaitrpc.notify('echo',123))print('#5',awaitrpc.get_methods())print('#6',awaitrpc.batch([['echo',2],'echo2','ping',]))loop=asyncio.get_event_loop()loop.run_until_complete(run())This prints:#1 pong
#2 {'args': ['one', 'two'], 'kwargs': {}}
#3 {'args': [], 'kwargs': {'three': '3'}}
#4 None
#5 {'get_method': {'doc': None, 'args': ['name'], 'kwargs': []}, 'get_methods': {'doc': None, 'args': [], 'kwargs': []}, 'ping': {'doc': None, 'args': ['rpc_request'], 'kwargs': []}, 'echo': {'doc': None, 'args': [], 'kwargs': []}}
#6 ({'args': [2], 'kwargs': {}}, JsonRpcError(-32601, 'The method does not exist / is not available.'), 'pong')back to top↑ This is enough to start :sunglasses: ↑IntegrationThe purpose of this library is to simplify life, and not vice versa.
And so, when you start adding existing functions, some problems may arise.Existing functions can return objects that are not serialized, but this is easy to fix.
You can write ownjson_serialize:fromaiohttpimportwebimportaiohttp_rpcimportuuidimportjsonfromdataclassesimportdataclassfromfunctoolsimportpartial@dataclassclassUser:# The object that is not serializable.uuid:uuid.UUIDusername:str='mike'email:str='some@mail.com'asyncdefget_user_by_uuid(user_uuid)->User:# Some function which returns not serializable object.# For example, data may be taken from a database.returnUser(uuid=uuid.UUID(user_uuid))defjson_serialize_unknown_value(value):ifisinstance(value,User):return{'uuid':str(value.uuid),'username':value.username,'email':value.email,}returnrepr(value)if__name__=='__main__':rpc_server=aiohttp_rpc.JsonRpcServer(json_serialize=partial(json.dumps,default=json_serialize_unknown_value),)rpc_server.add_method(get_user_by_uuid)app=web.Application()app.router.add_routes([web.post('/rpc',rpc_server.handle_http_request),])web.run_app(app,host='0.0.0.0',port=8080)..."""Example of response:{"id": 1,"jsonrpc": "2.0","result": {"uuid": "600d57b3-dda8-43d0-af79-3e81dbb344fa","username": "mike","email": "some@mail.com"}}"""But you can go further.
If you want to use functions that accept custom types,
then you can do something like this:# The function (RPC method) that takes a custom type.defgenerate_user_token(user:User):returnf'token-{str(user.uuid).split("-")[0]}'asyncdefreplace_type(data):ifnotisinstance(data,dict)or'__type__'notindata:returndataifdata['__type__']=='user':returnawaitget_user_by_uuid(data['uuid'])raiseaiohttp_rpc.errors.InvalidParams# The middleware that converts typesasyncdeftype_conversion_middleware(request,handler):request.set_args_and_kwargs(args=[awaitreplace_type(arg)forarginrequest.args],kwargs={key:awaitreplace_type(value)forkey,valueinrequest.kwargs.items()},)returnawaithandler(request)rpc_server=aiohttp_rpc.JsonRpcServer(middlewares=[aiohttp_rpc.middlewares.exception_middleware,aiohttp_rpc.middlewares.extra_args_middleware,type_conversion_middleware,])"""Request:{"id": 1234,"jsonrpc": "2.0","method": "generate_user_token","params": [{"__type__": "user", "uuid": "600d57b3-dda8-43d0-af79-3e81dbb344fa"}]}Response:{"id": 1234,"jsonrpc": "2.0","result": "token-600d57b3"}"""Middlewareallows you to replace arguments, responses, and more.If you want to add permission checking for each method,
then you can override the classJsonRpcMethodor usemiddleware.back to topMiddlewareMiddleware is used forRPC Request / RPC Responseprocessing.
It has a similar interface asaiohttp middleware.importaiohttp_rpcimporttypingasyncdefsimple_middleware(request:aiohttp_rpc.JsonRpcRequest,handler:typing.Callable)->aiohttp_rpc.JsonRpcResponse:# Code to be executed for each RPC request before# the method (and later middleware) are called.response=awaithandler(request)# Code to be executed for each RPC request / RPC response after# the method is called.returnresponserpc_server=aiohttp_rpc.JsonRpcServer(middlewares=[aiohttp_rpc.middlewares.exception_middleware,simple_middleware,])Or useaiohttp middlewaresto processweb.Request/web.Response.back to topWebSocketsWS Server Examplefromaiohttpimportwebimportaiohttp_rpcasyncdefping(rpc_request):return'pong'if__name__=='__main__':rpc_server=aiohttp_rpc.WsJsonRpcServer(middlewares=aiohttp_rpc.middlewares.DEFAULT_MIDDLEWARES,)rpc_server.add_method(ping)app=web.Application()app.router.add_routes([web.get('/rpc',rpc_server.handle_http_request),])app.on_shutdown.append(rpc_server.on_shutdown)web.run_app(app,host='0.0.0.0',port=8080)WS Client Exampleimportaiohttp_rpcimportasyncioasyncdefrun():asyncwithaiohttp_rpc.WsJsonRpcClient('http://0.0.0.0:8080/rpc')asrpc:print(awaitrpc.ping())print(awaitrpc.notify('ping'))print(awaitrpc.batch([['echo',2],'echo2','ping',]))loop=asyncio.get_event_loop()loop.run_until_complete(run())back to topAPI Referenceserverclass JsonRpcServer(BaseJsonRpcServer)def __init__(self, *, json_serialize=json_serialize, middlewares=(), methods=None)def add_method(self, method, *, replace=False) -> JsonRpcMethoddef add_methods(self, methods, replace=False) -> typing.List[JsonRpcMethod]def get_method(self, name) -> Optional[Mapping]def get_methods(self) -> Mapping[str, Mapping]async def handle_http_request(self, http_request: web.Request) -> web.Responseclass WsJsonRpcServer(BaseJsonRpcServer)rpc_server: JsonRpcServerclientclass JsonRpcClient(BaseJsonRpcClient)async def connect(self)async def disconnect(self)async def call(self, method: str, *args, **kwargs)async def notify(self, method: str, *args, **kwargs)async def batch(self, methods])async def batch_notify(self, methods)class WsJsonRpcClient(BaseJsonRpcClient)protocolclass JsonRpcRequestid: Union[int, str, None]method: strjsonrpc: strextra_args: MutableMappingcontext: MutableMappingparams: Anyargs: Optional[Sequence]kwargs: Optional[Mapping]is_notification: boolclass JsonRpcResponseid: Union[int, str, None]jsonrpc: strresult: Anyerror: Optional[JsonRpcError]context: MutableMappingclass JsonRpcMethod(BaseJsonRpcMethod)def __init__(self, func, *, name=None, add_extra_args=True, prepare_result=None)class JsonRpcUnlinkedResultsclass JsonRpcDuplicatedResultsdecoratorsdef rpc_method(*, rpc_server=default_rpc_server, name=None, add_extra_args=True)errorsclass JsonRpcError(RuntimeError)class ServerError(JsonRpcError)class ParseError(JsonRpcError)class InvalidRequest(JsonRpcError)class MethodNotFound(JsonRpcError)class InvalidParams(JsonRpcError)class InternalError(JsonRpcError)DEFAULT_KNOWN_ERRORSmiddlewaresasync def extra_args_middleware(request, handler)async def exception_middleware(request, handler)DEFAULT_MIDDLEWARESutilsdef json_serialize(*args, **kwargs)constantsNOTHINGVERSION_2_0back to topMore examplesThe library allows you to add methods in many ways:importaiohttp_rpcdefping_1(rpc_request):return'pong 1'defping_2(rpc_request):return'pong 2'defping_3(rpc_request):return'pong 3'rpc_server=aiohttp_rpc.JsonRpcServer()rpc_server.add_method(ping_1)# 'ping_1'rpc_server.add_method(aiohttp_rpc.JsonRpcMethod(ping_2))# 'ping_2'rpc_server.add_method(aiohttp_rpc.JsonRpcMethod(ping_3,name='third_ping'))# 'third_ping'rpc_server.add_methods([ping_3])# 'ping_3'# Replace methodrpc_server.add_method(ping_1,replace=True)# 'ping_1'rpc_server.add_methods([ping_1,ping_2],replace=True)# 'ping_1', 'ping_2'Example with built-in functions:# Serverimportaiohttp_rpcrpc_server=aiohttp_rpc.JsonRpcServer(middlewares=[aiohttp_rpc.middlewares.extra_args_middleware])rpc_server.add_method(sum)rpc_server.add_method(aiohttp_rpc.JsonRpcMethod(zip,prepare_result=list))...# Clientasyncwithaiohttp_rpc.JsonRpcClient('/rpc')asrpc:assertawaitrpc.sum([1,2,3])==6assertawaitrpc.zip(['a','b'],[1,2])==[['a',1],['b',2]]Example with the decorator:importaiohttp_rpcfromaiohttpimportweb@aiohttp_rpc.rpc_method()defecho(*args,**kwargs):return{'args':args,'kwargs':kwargs,}if__name__=='__main__':app=web.Application()app.router.add_routes([web.post('/rpc',aiohttp_rpc.rpc_server.handle_http_request),])web.run_app(app,host='0.0.0.0',port=8080)It is possible to pass params into aiohttp request viadirect_call/direct_batch:importaiohttp_rpcjsonrpc_request=aiohttp_rpc.JsonRpcRequest(method_name='test',params={'test_value':1})asyncwithaiohttp_rpc.JsonRpcClient('/rpc')asrpc:awaitrpc.direct_call(jsonrpc_request,headers={'My-Customer-Header':'custom value'},timeout=10)back to topLicenseMIT