Package: connecpy
ConnecpyPython implementation ofConnect Protocol.This repo contains a protoc plugin that generates sever and client code and a pypi package with common implementation details.InstallationYou can install the protoc plugin to generate files by running the command:goinstallgithub.com/i2y/connecpy/protoc-gen-connecpy@latestAdditionally, please add the connecpy package to your project using your preferred package manager. For instance, withRye, use the command:ryeaddconnecpyTo run the server, you'll need one of the following:Uvicorn,Daphne, orHypercorn. If your goal is to support both HTTP/1.1 and HTTP/2, you should opt for either Daphne or Hypercorn. Additionally, to test the server, you might need a client command, such asbuf.Generate and runUse the protoc plugin to generate connecpy server and client code.protoc--python_out=./--pyi_out=/.--connecpy_out=././haberdasher.protoServer code# service.pyimportrandomfromconnecpy.exceptionsimportInvalidArgumentfromconnecpy.contextimportServiceContextfromhaberdasher_pb2importHat,SizeclassHaberdasherService(object):asyncdefMakeHat(self,req:Size,ctx:ServiceContext)->Hat:print("remaining_time: ",ctx.time_remaining())ifreq.inches<=0:raiseInvalidArgument(argument="inches",error="I can't make a hat that small!")response=Hat(size=req.inches,color=random.choice(["white","black","brown","red","blue"]),)ifrandom.random()>0.5:response.name=random.choice(["bowler","baseball cap","top hat","derby"])returnresponse# server.pyfromconnecpyimportcontextfromconnecpy.asgiimportConnecpyASGIAppimporthaberdasher_connecpyfromserviceimportHaberdasherServiceservice=haberdasher_connecpy.HaberdasherServer(service=HaberdasherService())app=ConnecpyASGIApp()app.add_service(service)Run the server withuvicorn--port=3000server:appordaphne--port=3000server:apporhypercorn--bind:3000server:appClient code (Asyncronous)# async_client.pyimportasyncioimporthttpxfromconnecpy.contextimportClientContextfromconnecpy.exceptionsimportConnecpyServerExceptionimporthaberdasher_connecpy,haberdasher_pb2server_url="http://localhost:3000"timeout_s=5asyncdefmain():session=httpx.AsyncClient(base_url=server_url,timeout=timeout_s,)client=haberdasher_connecpy.AsyncHaberdasherClient(server_url,session=session)try:response=awaitclient.MakeHat(ctx=ClientContext(),request=haberdasher_pb2.Size(inches=12),# Optionally provide a session per request# session=session,)ifnotresponse.HasField("name"):print("We didn't get a name!")print(response)exceptConnecpyServerExceptionase:print(e.code,e.message,e.to_dict())finally:# Close the session (could also use a context manager)awaitsession.aclose()if__name__=="__main__":asyncio.run(main())Example output :size: 12
color: "black"
name: "bowler"Client code (Synchronous)# client.pyfromconnecpy.contextimportClientContextfromconnecpy.exceptionsimportConnecpyServerExceptionimporthaberdasher_connecpy,haberdasher_pb2server_url="http://localhost:3000"timeout_s=5defmain():client=haberdasher_connecpy.HaberdasherClient(server_url,timeout=timeout_s)try:response=client.MakeHat(ctx=ClientContext(),request=haberdasher_pb2.Size(inches=12),)ifnotresponse.HasField("name"):print("We didn't get a name!")print(response)exceptConnecpyServerExceptionase:print(e.code,e.message,e.to_dict())if__name__=="__main__":main()Other clientsOf course, you can use any HTTP client to make requests to a Connecpy server. For example, commands likecurlorbuf curlcan be used, as well as HTTP client libraries such asrequests,httpx,aiohttp, and others. The examples below usecurlandbuf curl.Content-Type: application/proto, HTTP/1.1bufcurl--data'{"inches": 12}'-vhttp://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat--schema./haberdasher.protoOn Windows, Content-Type: application/proto, HTTP/1.1bufcurl--data'{\"inches\": 12}'-vhttp://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat--schema.\haberdasher.protoContent-Type: application/proto, HTTP/2bufcurl--data'{"inches": 12}'-vhttp://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat--http2-prior-knowledge--schema./haberdasher.protoOn Windows, Content-Type: application/proto, HTTP/2bufcurl--data'{\"inches\": 12}'-vhttp://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat--http2-prior-knowledge--schema.\haberdasher.protoContent-Type: application/json, HTTP/1.1curl-XPOST-H"Content-Type: application/json"-d'{"inches": 12}'-vhttp://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHatOn Windows, Content-Type: application/json, HTTP/1.1curl-XPOST-H"Content-Type: application/json"-d'{\"inches\": 12}'-vhttp://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHatContent-Type: application/json, HTTP/2curl--http2-prior-knowledge-XPOST-H"Content-Type: application/json"-d'{"inches": 12}'-vhttp://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHatOn Windows, Content-Type: application/json, HTTP/2curl--http2-prior-knowledge-XPOST-H"Content-Type: application/json"-d'{\"inches\": 12}'-vhttp://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHatConnect ProtocolConnecpy protoc plugin generates the code based onConnect Protoclfrom the.protofiles.
Currently, Connecpy supports only Unary RPCs using the POST HTTP method. Connecpy will support other types of RPCs as well, in the near future.MiscServer Path PrefixYou can set server path prefix by passingserver_path_prefixtoConnecpyASGIAppconstructor.This example sets server path prefix to/foo/bar.# server.pyservice=haberdasher_connecpy.HaberdasherServer(service=HaberdasherService(),server_path_prefix="/foo/bar",)# async_client.pyresponse=awaitclient.MakeHat(ctx=ClientContext(),request=haberdasher_pb2.Size(inches=12),server_path_prefix="/foo/bar",)Interceptor (Server Side)ConnecpyASGIApp supports interceptors. You can add interceptors by passinginterceptorstoConnecpyASGIAppconstructor.
AsyncConnecpyServerInterceptor# server.pyfromtypingimportAny,Callablefromconnecpyimportcontextfromconnecpy.asgiimportConnecpyASGIAppfromconnecpy.interceptorimportAsyncConnecpyServerInterceptorimporthaberdasher_connecpyfromserviceimportHaberdasherServiceclassMyInterceptor(AsyncConnecpyServerInterceptor):def__init__(self,msg):self._msg=msgasyncdefintercept(self,method:Callable,request:Any,ctx:context.ServiceContext,method_name:str,)->Any:print("intercepting "+method_name+" with "+self._msg)returnawaitmethod(request,ctx)my_interceptor_a=MyInterceptor("A")my_interceptor_b=MyInterceptor("B")service=haberdasher_connecpy.HaberdasherServer(service=HaberdasherService())app=ConnecpyASGIApp(interceptors=(my_interceptor_a,my_interceptor_b),)app.add_service(service)Btw,ConnecpyServerInterceptor'sinterceptmethod has compatible signature asinterceptmethod ofgrpc_interceptor.server.AsyncServerInterceptor, so you might be able to convert Connecpy interceptors to gRPC interceptors by just changing the import statement and the parent class.ASGI MiddlewareYou can also use any ASGI middlewares with ConnecpyASGIApp.
The example bleow usesbrotli-asgi.# server.pyfrombrotli_asgiimportBrotliMiddlewarefromconnecpyimportcontextfromconnecpy.asgiimportConnecpyASGIAppimporthaberdasher_connecpyfromserviceimportHaberdasherServiceservice=haberdasher_connecpy.HaberdasherServer(service=HaberdasherService())app=ConnecpyASGIApp()app.add_service(service)app=BrotliMiddleware(app,minimum_size=1000)gRPC CompatibilityIn Connecpy, unlike connect-go, it is not possible to simultaneously support both gRPC and Connect RPC on the same server and port. In addition to it, Connecpy itself doesn't support gRPC. However, implementing a gRPC server using the same service code used for Connecpy server is feasible, as shown below. This is possible because the type signature of the service class in Connecpy is compatible with type signature gRPC farmework requires.
The example below usesgrpc.aioand there are inexample dicrectory.# grpc_server.pyimportasynciofromgrpc.aioimportserverimporthaberdasher_pb2_grpc# same service.py as the one used in previous server.pyfromserviceimportHaberdasherServicehost="localhost:50051"asyncdefmain():s=server()haberdasher_pb2_grpc.add_HaberdasherServicer_to_server(HaberdasherService(),s)bound_port=s.add_insecure_port(host)print(f"localhost:{bound_port}")awaits.start()awaits.wait_for_termination()if__name__=="__main__":asyncio.run(main())# grpc_client.pyimportasynciofromtypingimportcastfromgrpc.aioimportinsecure_channelimporthaberdasher_pb2target="localhost:50051"asyncdefmain():channel=insecure_channel(target)make_hat=channel.unary_unary("/i2y.connecpy.example.Haberdasher/MakeHat",request_serializer=haberdasher_pb2.Size.SerializeToString,response_deserializer=haberdasher_pb2.Hat.FromString,)request=haberdasher_pb2.Size(inches=12)response=cast(haberdasher_pb2.Hat,awaitmake_hat(request))print(response)if__name__=="__main__":asyncio.run(main())Message Body LengthCurrently, message body length limit is set to 100kb, you can override this by passingmax_receive_message_lengthtoConnecpyASGIAppconstructor.# this sets max message length to be 10 bytesapp=ConnecpyASGIApp(max_receive_message_length=10)Standing on the shoulders of giantsThe initial version (1.0.0) of this software was created by modifyinghttps://github.com/verloop/twirpyat January 4, 2024, so that it supports Connect Protocol. Therefore, this software is also licensed under Unlicense same as twirpy.