Package: pjrpc
pjrpcis an extensibleJSON-RPCclient/server library with an intuitive interface
that can be easily extended and integrated in your project without writing a lot of boilerplate code.Features:framework agnosticintuitive apiextendabilitysynchronous and asynchronous client backedsynchronous and asynchronous server supportpopular frameworks integrationbuiltin parameter validationpytest integrationopenapi schema generation supportweb ui support (SwaggerUI, RapiDoc, ReDoc)InstallationYou can install pjrpc with pip:$pipinstallpjrpcExtra requirementsaiohttpaio_pikaflaskjsonschemakombupydanticrequestshttpxopenapi-ui-bundlesstarlettedjangoDocumentationDocumentation is available atRead the Docs.QuickstartClient requestspjrpcclient interface is very simple and intuitive. Methods may be called by name, using proxy object
or by sending handmadepjrpc.common.Requestclass object. Notification requests can be made usingpjrpc.client.AbstractClient.notifymethod or by sending apjrpc.common.Requestobject without id.importpjrpcfrompjrpc.client.backendimportrequestsaspjrpc_clientclient=pjrpc_client.Client('http://localhost/api/v1')response:pjrpc.Response=client.send(pjrpc.Request('sum',params=[1,2],id=1))print(f"1 + 2 ={response.result}")result=client('sum',a=1,b=2)print(f"1 + 2 ={result}")result=client.proxy.sum(1,2)print(f"1 + 2 ={result}")client.notify('tick')Asynchronous client api looks pretty much the same:importpjrpcfrompjrpc.client.backendimportaiohttpaspjrpc_clientclient=pjrpc_client.Client('http://localhost/api/v1')response=awaitclient.send(pjrpc.Request('sum',params=[1,2],id=1))print(f"1 + 2 ={response.result}")result=awaitclient('sum',a=1,b=2)print(f"1 + 2 ={result}")result=awaitclient.proxy.sum(1,2)print(f"1 + 2 ={result}")awaitclient.notify('tick')Batch requestsBatch requests also supported. You can buildpjrpc.common.BatchRequestrequest by your hand and then send it to the
server. The result is apjrpc.common.BatchResponseinstance you can iterate over to get all the results or get
each one by index:importpjrpcfrompjrpc.client.backendimportrequestsaspjrpc_clientclient=pjrpc_client.Client('http://localhost/api/v1')batch_response=awaitclient.batch.send(pjrpc.BatchRequest(pjrpc.Request('sum',[2,2],id=1),pjrpc.Request('sub',[2,2],id=2),pjrpc.Request('div',[2,2],id=3),pjrpc.Request('mult',[2,2],id=4),))print(f"2 + 2 ={batch_response[0].result}")print(f"2 - 2 ={batch_response[1].result}")print(f"2 / 2 ={batch_response[2].result}")print(f"2 * 2 ={batch_response[3].result}")There are also several alternative approaches which are a syntactic sugar for the first one (note that the result
is not apjrpc.common.BatchResponseobject anymore but a tuple of “plain” method invocation results):using chain call notation:result=awaitclient.batch('sum',2,2)('sub',2,2)('div',2,2)('mult',2,2).call()print(f"2 + 2 ={result[0]}")print(f"2 - 2 ={result[1]}")print(f"2 / 2 ={result[2]}")print(f"2 * 2 ={result[3]}")using subscription operator:result=awaitclient.batch[('sum',2,2),('sub',2,2),('div',2,2),('mult',2,2),]print(f"2 + 2 ={result[0]}")print(f"2 - 2 ={result[1]}")print(f"2 / 2 ={result[2]}")print(f"2 * 2 ={result[3]}")using proxy chain call:result=awaitclient.batch.proxy.sum(2,2).sub(2,2).div(2,2).mult(2,2).call()print(f"2 + 2 ={result[0]}")print(f"2 - 2 ={result[1]}")print(f"2 / 2 ={result[2]}")print(f"2 * 2 ={result[3]}")Which one to use is up to you but be aware that if any of the requests returns an error the result of the other ones
will be lost. In such case the first approach can be used to iterate over all the responses and get the results of
the succeeded ones like this:importpjrpcfrompjrpc.client.backendimportrequestsaspjrpc_clientclient=pjrpc_client.Client('http://localhost/api/v1')batch_response=client.batch.send(pjrpc.BatchRequest(pjrpc.Request('sum',[2,2],id=1),pjrpc.Request('sub',[2,2],id=2),pjrpc.Request('div',[2,2],id=3),pjrpc.Request('mult',[2,2],id=4),))forresponseinbatch_response:ifresponse.is_success:print(response.result)else:print(response.error)Batch notifications:importpjrpcfrompjrpc.client.backendimportrequestsaspjrpc_clientclient=pjrpc_client.Client('http://localhost/api/v1')client.batch.notify('tick').notify('tack').notify('tick').notify('tack').call()Serverpjrpcsupports popular backend frameworks likeaiohttp,flaskand message brokers likekombuandaio_pika.Running of aiohttp based JSON-RPC server is a very simple process. Just define methods, add them to the
registry and run the server:importuuidfromaiohttpimportwebimportpjrpc.serverfrompjrpc.server.integrationimportaiohttpmethods=pjrpc.server.MethodRegistry()@methods.add(context='request')asyncdefadd_user(request:web.Request,user:dict):user_id=uuid.uuid4().hexrequest.app['users'][user_id]=userreturn{'id':user_id,**user}jsonrpc_app=aiohttp.Application('/api/v1')jsonrpc_app.dispatcher.add_methods(methods)jsonrpc_app.app['users']={}if__name__=="__main__":web.run_app(jsonrpc_app.app,host='localhost',port=8080)Parameter validationVery often besides dumb method parameters validation it is necessary to implement more “deep” validation and provide
comprehensive errors description to clients. Fortunatelypjrpchas builtin parameter validation based onpydanticlibrary which uses python type annotation for validation.
Look at the following example: all you need to annotate method parameters (or describe more complex types beforehand if
necessary).pjrpcwill be validating method parameters and returning informative errors to clients.importenumimportuuidfromtypingimportListimportpydanticfromaiohttpimportwebimportpjrpc.serverfrompjrpc.server.validatorsimportpydanticasvalidatorsfrompjrpc.server.integrationimportaiohttpmethods=pjrpc.server.MethodRegistry()validator=validators.PydanticValidator()classContactType(enum.Enum):PHONE='phone'EMAIL='email'classContact(pydantic.BaseModel):type:ContactTypevalue:strclassUser(pydantic.BaseModel):name:strsurname:strage:intcontacts:List[Contact]@methods.add(context='request')@validator.validateasyncdefadd_user(request:web.Request,user:User):user_id=uuid.uuid4()request.app['users'][user_id]=userreturn{'id':user_id,**user.dict()}classJSONEncoder(pjrpc.server.JSONEncoder):defdefault(self,o):ifisinstance(o,uuid.UUID):returno.hexifisinstance(o,enum.Enum):returno.valuereturnsuper().default(o)jsonrpc_app=aiohttp.Application('/api/v1',json_encoder=JSONEncoder)jsonrpc_app.dispatcher.add_methods(methods)jsonrpc_app.app['users']={}if__name__=="__main__":web.run_app(jsonrpc_app.app,host='localhost',port=8080)Error handlingpjrpcimplements all the errors listed inprotocol specificationwhich can be found inpjrpc.common.exceptionsmodule so that error handling is very simple and “pythonic-way”:importpjrpcfrompjrpc.client.backendimportrequestsaspjrpc_clientclient=pjrpc_client.Client('http://localhost/api/v1')try:result=client.proxy.sum(1,2)exceptpjrpc.MethodNotFoundase:print(e)Default error list can be easily extended. All you need to create an error class inherited frompjrpc.exc.JsonRpcErrorand define an error code and a description message.pjrpcwill be automatically
deserializing custom errors for you:importpjrpcfrompjrpc.client.backendimportrequestsaspjrpc_clientclassUserNotFound(pjrpc.exc.JsonRpcError):code=1message='user not found'client=pjrpc_client.Client('http://localhost/api/v1')try:result=client.proxy.get_user(user_id=1)exceptUserNotFoundase:print(e)On the server side everything is also pretty straightforward:importuuidimportflaskimportpjrpcfrompjrpc.serverimportMethodRegistryfrompjrpc.server.integrationimportflaskasintegrationapp=flask.Flask(__name__)methods=pjrpc.server.MethodRegistry()classUserNotFound(pjrpc.exc.JsonRpcError):code=1message='user not found'@methods.adddefadd_user(user:dict):user_id=uuid.uuid4().hexflask.current_app.users[user_id]=userreturn{'id':user_id,**user}@methods.adddefget_user(self,user_id:str):user=flask.current_app.users.get(user_id)ifnotuser:raiseUserNotFound(data=user_id)returnuserjson_rpc=integration.JsonRPC('/api/v1')json_rpc.dispatcher.add_methods(methods)app.users={}json_rpc.init_app(app)if__name__=="__main__":app.run(port=80)Open API specificationpjrpchas built-inOpenAPIandOpenRPCspecification generation support and integrated web UI as an extra dependency. Three UI types are supported:SwaggerUI (https://swagger.io/tools/swagger-ui/)RapiDoc (https://mrin9.github.io/RapiDoc/)ReDoc (https://github.com/Redocly/redoc)Web UI extra dependency can be installed using the following code:$pipinstallpjrpc[openapi-ui-bundles]The following example illustrates how to configure OpenAPI specification generation
and Swagger UI web tool with basic auth:importuuidfromtypingimportAny,Optionalimportflaskimportflask_httpauthimportpydanticimportflask_corsfromwerkzeugimportsecurityimportpjrpc.server.specs.extractors.pydanticfrompjrpc.server.integrationimportflaskasintegrationfrompjrpc.server.validatorsimportpydanticasvalidatorsfrompjrpc.server.specsimportextractors,openapiasspecsapp=flask.Flask('myapp')flask_cors.CORS(app,resources={"/myapp/api/v1/*":{"origins":"*"}})methods=pjrpc.server.MethodRegistry()validator=validators.PydanticValidator()auth=flask_httpauth.HTTPBasicAuth()credentials={"admin":security.generate_password_hash("admin")}@auth.verify_passworddefverify_password(username:str,password:str)->Optional[str]:ifusernameincredentialsandsecurity.check_password_hash(credentials.get(username),password):returnusernameclassAuthenticatedJsonRPC(integration.JsonRPC):@auth.login_requireddef_rpc_handle(self,dispatcher:pjrpc.server.Dispatcher)->flask.Response:returnsuper()._rpc_handle(dispatcher)classJSONEncoder(pjrpc.JSONEncoder):defdefault(self,o:Any)->Any:ifisinstance(o,pydantic.BaseModel):returno.dict()ifisinstance(o,uuid.UUID):returnstr(o)returnsuper().default(o)classUserIn(pydantic.BaseModel):"""
    User registration data.
    """name:strsurname:strage:intclassUserOut(UserIn):"""
    Registered user data.
    """id:uuid.UUIDclassAlreadyExistsError(pjrpc.exc.JsonRpcError):"""
    User already registered error.
    """code=2001message="user already exists"classNotFoundError(pjrpc.exc.JsonRpcError):"""
    User not found error.
    """code=2002message="user not found"@specs.annotate(tags=['users'],errors=[AlreadyExistsError],examples=[specs.MethodExample(summary="Simple example",params=dict(user={'name':'John','surname':'Doe','age':25,},),result={'id':'c47726c6-a232-45f1-944f-60b98966ff1b','name':'John','surname':'Doe','age':25,},),],)@methods.add@validator.validatedefadd_user(user:UserIn)->UserOut:"""
    Creates a user.

    :param object user: user data
    :return object: registered user
    :raise AlreadyExistsError: user already exists
    """forexisting_userinflask.current_app.users_db.values():ifuser.name==existing_user.name:raiseAlreadyExistsError()user_id=uuid.uuid4().hexflask.current_app.users_db[user_id]=userreturnUserOut(id=user_id,**user.dict())@specs.annotate(tags=['users'],errors=[NotFoundError],examples=[specs.MethodExample(summary='Simple example',params=dict(user_id='c47726c6-a232-45f1-944f-60b98966ff1b',),result={'id':'c47726c6-a232-45f1-944f-60b98966ff1b','name':'John','surname':'Doe','age':25,},),],)@methods.add@validator.validatedefget_user(user_id:uuid.UUID)->UserOut:"""
    Returns a user.

    :param object user_id: user id
    :return object: registered user
    :raise NotFoundError: user not found
    """user=flask.current_app.users_db.get(user_id.hex)ifnotuser:raiseNotFoundError()returnUserOut(id=user_id,**user.dict())@specs.annotate(tags=['users'],errors=[NotFoundError],examples=[specs.MethodExample(summary='Simple example',params=dict(user_id='c47726c6-a232-45f1-944f-60b98966ff1b',),result=None,),],)@methods.add@validator.validatedefdelete_user(user_id:uuid.UUID)->None:"""
    Deletes a user.

    :param object user_id: user id
    :raise NotFoundError: user not found
    """user=flask.current_app.users_db.pop(user_id.hex,None)ifnotuser:raiseNotFoundError()json_rpc=AuthenticatedJsonRPC('/api/v1',json_encoder=JSONEncoder,spec=specs.OpenAPI(info=specs.Info(version="1.0.0",title="User storage"),servers=[specs.Server(url='http://127.0.0.1:8080',),],security_schemes=dict(basicAuth=specs.SecurityScheme(type=specs.SecuritySchemeType.HTTP,scheme='basic',),),security=[dict(basicAuth=[]),],schema_extractor=extractors.pydantic.PydanticSchemaExtractor(),ui=specs.SwaggerUI(),# ui=specs.RapiDoc(),# ui=specs.ReDoc(),),)json_rpc.dispatcher.add_methods(methods)app.users_db={}myapp=flask.Blueprint('myapp',__name__,url_prefix='/myapp')json_rpc.init_app(myapp)app.register_blueprint(myapp)if__name__=="__main__":app.run(port=8080)Specification is available onhttp://localhost:8080/myapp/api/v1/openapi.jsonWeb UI is running onhttp://localhost:8080/myapp/api/v1/ui/Swagger UI:RapiDoc:Redoc: