Package: xjsonrpc
pjrpc upstream status:bernhardkaindl/xjsonrpc status:xjsonrpcis 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.It was forked fromhttps://github.com/dapper91/pjrpcto make fast progress in
the area of the rabbitmq/aio_pika backend with fixes and improved code examples.Features:framework agnosticintuitive apiextendabilitysynchronous and asynchronous client backendsynchronous and asynchronous server supportpopular frameworks integrationbuiltin parameter validationpytest integrationopenapi schema generation supportweb ui support (SwaggerUI, RapiDoc, ReDoc)InstallationYou can install xjsonrpc with pip:$pipinstallxjsonrpcExtra requirementsaiohttpaio_pikaflaskjsonschemakombupydanticrequestshttpxopenapi-ui-bundlesstarlettedjangoDocumentationDocumentation is available atRead the Docs.QuickstartClient requestsxjsonrpcclient interface is very simple and intuitive. Methods may be called by name, using proxy object
or by sending handmadexjsonrpc.common.Requestclass object. Notification requests can be made usingxjsonrpc.client.AbstractClient.notifymethod or by sending axjsonrpc.common.Requestobject without id.importxjsonrpcfromxjsonrpc.client.backendimportrequestsasxjsonrpc_clientclient=xjsonrpc_client.Client('http://localhost/api/v1')response:xjsonrpc.Response=client.send(xjsonrpc.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:importxjsonrpcfromxjsonrpc.client.backendimportaiohttpasxjsonrpc_clientclient=xjsonrpc_client.Client('http://localhost/api/v1')response=awaitclient.send(xjsonrpc.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 buildxjsonrpc.common.BatchRequestrequest by your hand and then send it to the
server. The result is axjsonrpc.common.BatchResponseinstance you can iterate over to get all the results or get
each one by index:importxjsonrpcfromxjsonrpc.client.backendimportrequestsasxjsonrpc_clientclient=xjsonrpc_client.Client('http://localhost/api/v1')batch_response=awaitclient.batch.send(xjsonrpc.BatchRequest(xjsonrpc.Request('sum',[2,2],id=1),xjsonrpc.Request('sub',[2,2],id=2),xjsonrpc.Request('div',[2,2],id=3),xjsonrpc.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 axjsonrpc.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:importxjsonrpcfromxjsonrpc.client.backendimportrequestsasxjsonrpc_clientclient=xjsonrpc_client.Client('http://localhost/api/v1')batch_response=client.batch.send(xjsonrpc.BatchRequest(xjsonrpc.Request('sum',[2,2],id=1),xjsonrpc.Request('sub',[2,2],id=2),xjsonrpc.Request('div',[2,2],id=3),xjsonrpc.Request('mult',[2,2],id=4),))forresponseinbatch_response:ifresponse.is_success:print(response.result)else:print(response.error)Batch notifications:importxjsonrpcfromxjsonrpc.client.backendimportrequestsasxjsonrpc_clientclient=xjsonrpc_client.Client('http://localhost/api/v1')client.batch.notify('tick').notify('tack').notify('tick').notify('tack').call()Serverxjsonrpcsupports 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:importuuidfromaiohttpimportwebimportxjsonrpc.serverfromxjsonrpc.server.integrationimportaiohttpmethods=xjsonrpc.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. Fortunatelyxjsonrpchas 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).xjsonrpcwill be validating method parameters and returning informative errors to clients.importenumimportuuidfromtypingimportListimportpydanticfromaiohttpimportwebimportxjsonrpc.serverfromxjsonrpc.server.validatorsimportpydanticasvalidatorsfromxjsonrpc.server.integrationimportaiohttpmethods=xjsonrpc.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(xjsonrpc.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 handlingxjsonrpcimplements all the errors listed inprotocol specificationwhich can be found inxjsonrpc.common.exceptionsmodule so that error handling is very simple and “pythonic-way”:importxjsonrpcfromxjsonrpc.client.backendimportrequestsasxjsonrpc_clientclient=xjsonrpc_client.Client('http://localhost/api/v1')try:result=client.proxy.sum(1,2)exceptxjsonrpc.MethodNotFoundase:print(e)Default error list can be easily extended. All you need to create an error class inherited fromxjsonrpc.exc.JsonRpcErrorand define an error code and a description message.xjsonrpcwill be automatically
deserializing custom errors for you:importxjsonrpcfromxjsonrpc.client.backendimportrequestsasxjsonrpc_clientclassUserNotFound(xjsonrpc.exc.JsonRpcError):code=1message='user not found'client=xjsonrpc_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:importuuidimportflaskimportxjsonrpcfromxjsonrpc.serverimportMethodRegistryfromxjsonrpc.server.integrationimportflaskasintegrationapp=flask.Flask(__name__)methods=xjsonrpc.server.MethodRegistry()classUserNotFound(xjsonrpc.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 specificationxjsonrpchas 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:$pipinstallxjsonrpc[openapi-ui-bundles]The following example illustrates how to configure OpenAPI specification generation
and Swagger UI web tool with basic auth:importuuidfromtypingimportAny,Optionalimportflaskimportflask_httpauthimportpydanticimportflask_corsfromwerkzeugimportsecurityimportxjsonrpc.server.specs.extractors.pydanticfromxjsonrpc.server.integrationimportflaskasintegrationfromxjsonrpc.server.validatorsimportpydanticasvalidatorsfromxjsonrpc.server.specsimportextractors,openapiasspecsapp=flask.Flask('myapp')flask_cors.CORS(app,resources={"/myapp/api/v1/*":{"origins":"*"}})methods=xjsonrpc.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:xjsonrpc.server.Dispatcher)->flask.Response:returnsuper()._rpc_handle(dispatcher)classJSONEncoder(xjsonrpc.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(xjsonrpc.exc.JsonRpcError):"""
    User already registered error.
    """code=2001message="user already exists"classNotFoundError(xjsonrpc.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: