Python

Using gRPC in Python

gRPC is an HTTP/2-based Remote Procedure Call (RPC) framework that uses protocol buffers (protobuf) as the underlying data serialization framework. It is an alternative to other language-neutral RPC frameworks such as Apache Thrift and Apache Arvo.

In the first part of this series on using gRPC in Python, we will implement a gRPC service in Python. Our gRPC service will be a users service that will expose two functionalities: user signup and getting user details. In our fictional scenario, we will be interacting with this gRPC service directly from a command-line program and also from an HTTP web application.

Some of the gRPC concepts we will explore in this article are:

  • Streaming responses
  • Setting client-side metadata
  • Client-side timeouts

We will be using Python 3.5 for our demos in the article. The git repository has all the code listings we will be discussing in this article in the subdirectory demo1.

Service Specification

The first step to implementing a gRPC server is to describe the server’s interface. The interface of the service is defined by the functions it exposes and the input and output messages. Our example service is called Users and defines one function to create a user and another to fetch user details. The following defines the service Users with two functions via protobuf:

// users.proto
syntax = "proto3"; 
import "users_types.proto";
service Users { 
  rpc CreateUser (users.CreateUserRequest) returns (users.CreateUserResult); 
  rpc GetUsers (users.GetUsersRequest) returns (stream  users.GetUsersResult); 
}

syntax "proto3" declares that we are using the proto3 version of the protocol buffer’s language. Using import, we import message definitions defined in a separate file. gRPC or protocol buffers does not require us to do this, and it is a purely personal preference to keep the service definitions and the message types separate.

The CreateUser function accepts a request of type CreateUserRequest defined in the users package and returns a result of type CreateUserResult defined in the same package. This function simulates the user-signup functionality of our Users service.

The GetUsers function accepts a request of type GetUsersRequest and returns a stream of GetUsersResult objects. Specifying the output as a stream allows us to return part of the complete response to the client as soon as they are ready. This function simulates the functionality of returning the user details of a given user or multiple users.

The user_types.proto file imported above defines the messages. You can see the entire file in the demo1/grpc-services/protos/users/ subdirectory. It starts off by defining proto3 syntax as above. However, it also defines a package:

syntax = "proto3"; 
package users;

Defining a new package allows us to define a namespace and hence we don’t have to worry about not using the same message names across different services.

Next, we define the message User describing a user in our service:

message User { 
  string username = 1; 
  uint32 user_id = 2; 
}

We then define the message we’ll use to return the result of a create-user operation:

message CreateUserResult { 
  User user = 1; 
}

Above, we define the CreateUserResult message to have a single field of type User (defined earlier). The GetUsers function allows querying multiple users at once. Hence we define the GetUsesRequest message as follows:

message GetUsersRequest { 
  repeated User user = 1; 
}

When a field in a message is defined as repeated, the message can have this field zero or more times. In Python, it is equivalent to defining a list of users.

Next, use the service and types definition to generate language-specific bindings that will allow us to implement servers to use the above service and clients to talk to the server. Before we can do that however, we will create Python 3.5 virtual environment, activate it, and install two packages, grpcio and grpcio-tools:

$ python3.5 -m venv ~/.virtualenvs/grpc 
$ . ~/.virtualenvs/grpc/activate 
$ python -m pip install grpcio grpcio-tools

Writing the Server

Let’s generate the Python bindings from the above protobuf definition:

$ cd demo1/grpc-services/protos/ 
$ ./build.sh

The build.sh is a bash script that wraps around the grpc-tools package and essentially does the following:

$ cd users 
$ mkdir ../gen-py 
$ python -m grpc_tools.protoc 
    \ --proto_path=.
    \ --python_out=../gen-py 
    \ --grpc_python_out=../gen-py 
    \ *.proto

The arguments serve different purposes:

  • proto_path: Path to look for the protobuf definitions
  • python_out: Directory to generate the protobuf Python code
  • grpc_python_out: Directory to generate the gRPC Python code

The last argument specifies the protobuf files to compile.

The created gen-py directory will have the following four generated files:

- users_types_pb2.py
- users_types_pb2_grpc.py
- users_pb2.py
- users_pb2_grpc.py

The *_pb2.py files has the code corresponding to the types we have defined and the *_grpc.py files has the generated code related to the functions we have defined.

Using the above generated code, we will write our “servicer” code:

import users_pb2_grpc as users_service 
import users_types_pb2 as users_messages

class UsersService(users_service.UsersServicer):

    def CreateUser(self, request, context):
        metadata = dict(context.invocation_metadata())
        print(metadata)
        user = users_messages.User(username=request.username, user_id=1)
        return users_messages.CreateUserResult(user=user)

    def GetUsers(self, request, context):
        for user in request.user:
            user = users_messages.User(
                username=user.username, user_id=user.user_id
            )
            yield users_messages.GetUsersResult(user=user)

The UsersService subclasses the UsersServicer class generated in users_pb2_grpc and implements the two functions implemented by our service CreateUser and GetUsers. Each function accepts two arguments: request referring to the incoming request message and context giving access to various contextual data.

In the CreateUser function, we see an example of accessing the metadata associated with the request. metadata is a list of arbitrary key-value pairs that the client can send along with a request and is part of the gRPC specification itself. We access it by calling the invocation_metadata() method of the context object.

The reason we need the explicit dict conversion is because they are returned as a list of tuples. Since the CreateUser function merely simulates a user-signup functionality, we just return back a new User object, with the username being the same as that provided in the request (a CreateUser object) accessible via request.username and set the user_id as 1. However, we do not send the newly created user object on its own but wrap it in a CreateUserResult object.

The GetUsers function simulates the funcionality of querying user details. Recall that our GetUsersRequest object declares the user field as repeated. Hence, we iterate over the user field, with each item being an User object. We access the username and user_id from the object and use those to create a new User object.

The output of the GetUsers function was declared to be a stream of GetUsersResult messages. Hence, we wrap the created User object in a GetUsersResult and yield it back to the client.

The next step is to set up our server code to be able to process incoming gRPC requests and hand it over to our UsersService class:

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    users_service.add_UsersServicer_to_server(UsersService(), server)
    server.add_insecure_port('127.0.0.1:50051')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)

The grpc.Server function creates a server. We call it above with the only required argument, a futures.ThreadPoolExecutor with the maximum number of workers set to 10. Then we call the add_UsersServicer_to_server function to register UsersService with the server.

Using add_insecure_port, we set up the listening IP address and port and then start the server using the start() method. The add_insecure_port function is used since we are not setting SSL/TLS for our client/server communication and authentication.

The code for the server can be found in demo1/grpc-services/users/server/server.py.

Let’s now start our server:

$ cd demo1/grpc-services/users/
$ ./run-server.sh

The run-server.sh bash script is basically:

$ PYTHONPATH=../../protos/gen-py/ python server.py

!Sign up for a free Codeship Account

Writing a Client

To interact with our above server, a gRPC client will first create a channel to our server and create a stub object to interact with the users service via this channel:

channel = grpc.insecure_channel('localhost:50051')
try:
    grpc.channel_ready_future(channel).result(timeout=10)
except grpc.FutureTimeoutError:
    sys.exit('Error connecting to server')
else:
    stub = users_service.UsersStub(channel)

The channel_ready_future function allows the client to wait for a specified timeout duration (in seconds) for the server to be ready. If our client times out, we exit; else, we create a UsersStub object passing the channel created as an argument.

Using this stub object, we invoke a gRPC call as follows:

metadata = [('ip', '127.0.0.1')]
response = stub.CreateUser(
    users_messages.CreateUserRequest(username='tom'),
    metadata=metadata,
)
if response:
    print("User created:", response.user.username)

Above, we see a demonstration of how we pass additional metadata as part of a request. In a realistic scenario, we will pass in data such as a request ID, the authenticated user ID making the request, etc.

Next, we see how we create a message that has a field repeated twice:

request = users_messages.GetUsersRequest(
    user=[users_messages.User(username="alexa", user_id=1),
          users_messages.User(username="christie", user_id=1)]
)

Since the GetUsersRequest message specified the field user as repeated, we create a list of User objects and set it as the value of this field.

The result of the GetUsers function is a stream. Hence, we use the following construct to access the responses:

response = users_service.GetUsers(request)
for resp in response:
    print(resp)

Let’s now run the client in the demo1/grpc-services/users/sample_client_demo.py file:

$ cd demo1/grpc-services/users 
$ PYTHONPATH=../protos/gen-py/ python sample_client_demo.py 
User created: tom
user {
  username: "alexa"
  user_id: 1
}

user {
  username: "christie"
  user_id: 1
}

On the server side, we will see the metadata being printed for the incoming client request:

{'ip': '127.0.0.1', 'user-agent': 'grpc-python/1.6.0 grpc-c/4.0.0 (manylinux; chttp2; garcia)'}

Setting Client-side Timeout

When making a request, we can set client-side timeouts so that our client doesn’t get into a state where it’s waiting for a long time for a request to complete. There’s no way currently to set a stub-wide timeout; therefore, we have to specify the timeout per call, like so:

response = stub.GetUsers(request, timeout=30)

Above, we set the timeout to 30 seconds. If the client doesn’t get a response from the server within 30 seconds, it will raise a grpc.RpcError exception with the status code DEADLINE_EXCEEDED. Ideally, we will catch the exception and report a user-friendly error message.

Demo Web Application Talking to gRPC

The demo1/webapp directory contains a Flask application exporting a single endpoint /users/ that invokes the GetUsers gRPC function and streams the responses back:

@app.route('/users/')
def users_get():
    request = users_messages.GetUsersRequest(
        user=[users_messages.User(username="alexa", user_id=1),
              users_messages.User(username="christie", user_id=1)]
    )
    def get_user():
        response = app.config['users'].GetUsers(request)
        for resp in response:
            yield MessageToJson(resp)
    return Response(get_user(), content_type='application/json')

Since the response from our gRPC function is a stream, we create a generator function get_user() to yield a response as we get one. We use the MessageToJson function from the protobuf Python package to convert a protobuf message to a JSON object.

What’s Next?

In the next article, we will be enhancing our gRPC server and learning about:

  • Writing a secure gRPC server
  • Error handling
  • Writing interceptors and exporting metrics

The code listings for this article can be found in demo1 sub-directory in this git repo.

Resources

Published on Web Code Geeks with permission by Amit Saha, partner at our WCG program. See the original article here: Using gRPC in Python

Opinions expressed by Web Code Geeks contributors are their own.

Amit Saha

Amit Saha is a software engineer and author of Doing Math with Python. He's written for Linux Journal, Linux Voice, and Linux Magazine.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button