First glimpse into gRPC through Python (Part 2)

ivanyu2021

Ivan Yu

Posted on January 19, 2023

First glimpse into gRPC through Python (Part 2)

1. Previous Post

First glimpse into gRPC through Python (Part 1)

2. Implementation and Examples

After introduced gRPC concept and development flow in previous post, we will show how to implement gRPC in python.

Example Code link (To run the below examples, please go through README.md in the link)

2.1. 4 kinds of service method

There are 4 kinds of service methods in gRPC and the difference between them is to apply steaming or not in request or response.

2.1.1. Simple RPC

This type is just like the http request and response. Client send a single request to server and server reply a single reponse.

Proto file

rpc doSimple ( Request ) returns ( Response ) {}
Enter fullscreen mode Exit fullscreen mode

Server

def doSimple(self, request, context):
    output = f"Hello { request.input }!"
    return Response( output=f"Hello { request.input }!" )
Enter fullscreen mode Exit fullscreen mode

Client

def call_doSimple( self ):
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = SampleServiceStub( channel )
        request = Request( input=self.faker.name() )
        logger.info( f"doSimple client sent: { request.input }" )
        response = stub.doSimple( request )
        logger.info( f"doSimple client received: { response.output }" )
Enter fullscreen mode Exit fullscreen mode

Command line output

[ 2022-07-15 09:43:27,530 ][ INFO ][ call_doSimple ] doSimple client sent: Lindsay Ross
[ 2022-07-15 09:43:27,531 ][ INFO ][ call_doSimple ] doSimple client received: Hello Lindsay Ross!
Enter fullscreen mode Exit fullscreen mode

2.1.2. Response-streaming RPC

For this type, streaming happens in the response. Client sends a single request to server and server returns a streaming of multiple responses.

Proto file

rpc doResponseStreaming( Request ) returns ( stream Response ) {}
Enter fullscreen mode Exit fullscreen mode

Server

def doResponseStreaming(self, request, context):
    faker = Faker()
    name_list = [ *[ faker.name() for i in range( 3 ) ], request.input ]

    for name in name_list:
        time.sleep( 0.5 )
        yield Response( output=name )
Enter fullscreen mode Exit fullscreen mode

Client

def call_doResponseStreaming( self ):
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = SampleServiceStub( channel )
        request = Request( input=self.faker.name() )
        logger.info( f"doResponseStreaming client sent: { request.input }" )
        response_generator = stub.doResponseStreaming( request )
        for response in response_generator:
            logger.info( f"doResponseStreaming client received: { response.output }" )
Enter fullscreen mode Exit fullscreen mode

Command line output

[ 2022-07-15 10:14:27,347 ][ INFO ][ call_doResponseStreaming ] doResponseStreaming client sent: Veronica Good
[ 2022-07-15 10:14:27,971 ][ INFO ][ call_doResponseStreaming ] doResponseStreaming client received: Richard Torres
[ 2022-07-15 10:14:28,472 ][ INFO ][ call_doResponseStreaming ] doResponseStreaming client received: Monica Russo
[ 2022-07-15 10:14:28,985 ][ INFO ][ call_doResponseStreaming ] doResponseStreaming client received: Sean Lane
[ 2022-07-15 10:14:29,498 ][ INFO ][ call_doResponseStreaming ] doResponseStreaming client received: Veronica Good
Enter fullscreen mode Exit fullscreen mode

2.1.3. Request-streaming RPC

Similar to the above type, but the streaming happens in the request. Client sends a streaming of multiple requests to server and server returns a single response.

Proto file

rpc doRequestStreaming ( stream Request ) returns ( Response ) {}
Enter fullscreen mode Exit fullscreen mode

Server

def doRequestStreaming(self, request_iterator, context):
    result_list = []
    for request in request_iterator:
        result_list.append( request.input.upper() )

    return Response( output=", ".join( result_list ) )
Enter fullscreen mode Exit fullscreen mode

Client

def call_doRequestStreaming( self ):

    def get_fake_name_generator():
        faker = Faker()
        for _ in range( 10 ):
            time.sleep( 0.5 )
            name = faker.name()
            logger.info( f"doRequestStreaming client sent: { name }." )
            yield Request( input=name )

    with grpc.insecure_channel('localhost:50051') as channel:
        stub = SampleServiceStub( channel )
        request = get_fake_name_generator()
        response = stub.doRequestStreaming( request )
        logger.info( f"doRequestStreaming client received: { response.output }" )
Enter fullscreen mode Exit fullscreen mode

Command line output

[ 2022-07-15 10:21:08,058 ][ INFO ][ get_fake_name_generator ] doRequestStreaming client sent: Courtney Hammond.
[ 2022-07-15 10:21:08,562 ][ INFO ][ get_fake_name_generator ] doRequestStreaming client sent: James Petersen.
[ 2022-07-15 10:21:09,070 ][ INFO ][ get_fake_name_generator ] doRequestStreaming client sent: Tom Anderson.
[ 2022-07-15 10:21:09,073 ][ INFO ][ call_doRequestStreaming ] doRequestStreaming client received: COURTNEY HAMMOND, JAMES PETERSEN, TOM ANDERSON
Enter fullscreen mode Exit fullscreen mode

2.1.4. Bidirectionally-streaming RPC

As you may already guess the meaning from the name, the streaming happens in both request and response.

Proto file

rpc doBidirectional ( stream Request ) returns ( stream Response ) {}
Enter fullscreen mode Exit fullscreen mode

Server

def doBidirectional(self, request_iterator, context):
    for request in request_iterator:
        yield Response( output=request.input + " is excellent!" )
Enter fullscreen mode Exit fullscreen mode

Client

def call_doBidirectional( self ):

    def get_fake_name_generator():
        faker = Faker()
        for _ in range( 3 ):
            time.sleep( 0.5 )
            name = faker.name()
            logger.info( f"doRequestStreaming client sent: { name }." )
            yield Request( input=name )

    with grpc.insecure_channel('localhost:50051') as channel:
        stub = SampleServiceStub( channel )
        request = get_fake_name_generator()
        response_generator = stub.doBidirectional( request )
        for response in response_generator:
            logger.info( f"doBidirectional client received: { response.output }" )
Enter fullscreen mode Exit fullscreen mode

Command line output

[ 2022-07-15 10:41:11,994 ][ INFO ][ get_fake_name_generator ] doRequestStreaming client sent: Sherry Hanson.
[ 2022-07-15 10:41:11,996 ][ INFO ][ call_doBidirectional ] doBidirectional client received: Sherry Hanson is excellent!
[ 2022-07-15 10:41:12,497 ][ INFO ][ get_fake_name_generator ] doRequestStreaming client sent: Danielle Jones.
[ 2022-07-15 10:41:12,499 ][ INFO ][ call_doBidirectional ] doBidirectional client received: Danielle Jones is excellent!
[ 2022-07-15 10:41:12,999 ][ INFO ][ get_fake_name_generator ] doRequestStreaming client sent: Alexis Goodwin.
[ 2022-07-15 10:41:13,001 ][ INFO ][ call_doBidirectional ] doBidirectional client received: Alexis Goodwin is excellent!
Enter fullscreen mode Exit fullscreen mode

2.2. Special Data Types and default value

gRPC can handles some speical data types, like date time, list or map, but need to pay some attention to avoid bug.

2.2.1. Date Time

gRPC use timestamp to handle date time and it uses timezone.

In proto file

google.protobuf.Timestamp date = 1;
Enter fullscreen mode Exit fullscreen mode

You need to include timezone in the input date time in order to transfer the data correctly to the server side.

Below is the output send the date WITHOUT timezone from client to server side:

[ 2022-07-15 11:04:52,633 ][ INFO ][ call_doSpecialDataType ] doSpecialDataType client sent: request.date=2022-07-15 11:04:52
[ 2022-07-15 11:04:52,654 ][ INFO ][ doSpecialDataType ] doSpecialDataType Server received: request.date=2022-07-15 20:04:52
Enter fullscreen mode Exit fullscreen mode

We can see there are 9 hours difference between client and server since gRPC regards the date time without timezone as UTC time. When the date input received by the server, it automatically add 9 hours as we are in UTC+09:00 region.

2.2.2. List

gRPC can handle list datatype by simply adding repeated in front of the field in proto file.

repeated string names = 1;
Enter fullscreen mode Exit fullscreen mode

2.2.3. Map

gRPC can handle map datatype by defining map as the type and specify key and value type in proto file.

map<string, string> name2phoneNumMap = 3;
Enter fullscreen mode Exit fullscreen mode

2.2.4. Default value

According to this link, it mentioned that

if a scalar message field is set to its default, the value will not be serialized on the wire.

We will explain the meaning in the following example:

proto file

repeated CardInfo cardInfos = 4;

message CardInfo {
    string name = 1;
    int32 numberOfCreditCard = 2;
}
Enter fullscreen mode Exit fullscreen mode

We have a list of cardInfo and each card info contains an integer field called numberOfCreditCard. In the response, we set numberOfCreditCard of last CardInfo of the cardInfo list to be 0.

Server

cardInfos = request.cardInfos
cardInfos.append( CardInfo( name="Boris Lee", numberOfCreditCard=0 ) )
Enter fullscreen mode Exit fullscreen mode

Command line output

[ 2022-07-15 11:21:39,200 ][ INFO ][ call_doSpecialDataType ] doSpecialDataType client received:  response.cardInfos= [name: "Katherine Soto"
numberOfCreditCard: 1
, name: "Kerry Powell"
numberOfCreditCard: 1
, name: "Mrs. Christina Hicks DDS"
numberOfCreditCard: 1
, name: "Boris Lee" <- No "numberOfCreditCard"
]
Enter fullscreen mode Exit fullscreen mode

We can see that Boris Lee does NOT have numberOfCreditCard. Since 0 is regarded as default value, it will not be serialized on the wire and transfer back to client.

To solve this problem, we need to add optional in front of the field in proto file.

optional int32 numberOfCreditCard = 2;
Enter fullscreen mode Exit fullscreen mode

Generate the code and run the program again, you can see the zero appeared.

, name: "Boris Lee"
numberOfCreditCard: 0
]
Enter fullscreen mode Exit fullscreen mode

3. Unit test

We can use python native unit test framework to write unit test for gRPC. This is crucial as you do not need to switch on/off the gRPC server again and again whenever there is change in your code in order to test your code manually.

3.1. Create test server

First, you need to create a test server for receiving gRPC call in the setUp method so that whenever the test method is called, it will first set up the test server.

class SampleServiceTest(unittest.TestCase):

    def setUp(self):
        logger.info( f"=== Method: { self._testMethodName } =======" )
        servicers = {
            sample_service_pb2.DESCRIPTOR.services_by_name['SampleService']: SampleService()
        }

        self.test_server = grpc_testing.server_from_dictionary(
            servicers, grpc_testing.strict_real_time())
Enter fullscreen mode Exit fullscreen mode

3.2. Create test method

Next, you need to create at least one test method for each gRPC method you want to test.

def test_doSimple(self):
    faker = Faker()
    request = sample_service_pb2.Request( input=faker.name() )

    doSimple_method = self.test_server.invoke_unary_unary(
        method_descriptor=(sample_service_pb2.DESCRIPTOR
            .services_by_name['SampleService']
            .methods_by_name['doSimple']),
        invocation_metadata={},
        request=request, timeout=1)

    response, _, code, _ = doSimple_method.termination()
    self.assertEqual( code, grpc.StatusCode.OK )
    self.assertEqual( response.output, f"Hello { request.input }!" )
Enter fullscreen mode Exit fullscreen mode

3.3. Run test

Finally, run your test in the main thread

if __name__ == '__main__':
    unittest.main()
Enter fullscreen mode Exit fullscreen mode

For detail, take a look in sample_service_test.py

4. Reason I wrote this series of blogs

I wrote this blog as I found that when I first learnt about gRPC, it is a little bit difficult to grasp the development method in a fast manner. There are a lot of built-in examples in the gRPC site, but it is a little bit too complicate for a beginner.

At first, what I wanted to know the most are:

1. How can I make a new method?
2. How can I call the method in a microservice through gRPC?
3. How can I handle the input and output?
4. How can I do the unit test?
Enter fullscreen mode Exit fullscreen mode

There are resources in Internet, but they are either scattered around or they used a lot of words to explain without a diagram. As a result, I decided to write this blog to present gRPC (using Python) in a simple and practical way so newcomer can pick up the concept easily and then start development immediately.

💖 💪 🙅 🚩
ivanyu2021
Ivan Yu

Posted on January 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related