Python interoperability
One of the greatest gRPC features is its programming language-neutral nature. You can write gRPC services in C# and access them from other languages. The list includes pretty much everything used in software industry. Conversely, one can connect gRPC clients written in C# to service written in Java, Go, Ruby, you name it.
We are going to demonstrate these capabilities using Python. Python is one of the programming languages I really enjoy working with.
If you want to follow along, have Python installed. I am using version 3.9.
Setup
Make the python
directory on the same level as Shared
, Client
and Service
mkdir python
cd python
we are going to do all subsequent work in this article in the python
directory unless specified otherwise.
Next we will create a virtual environment, so all python packages we install will be confined to that virtual environment and not interfere with the system-installed packages.
python -m venv .venv
Next, activate the virtual environment On Mac and Linux:
source ./.venv/bin/activate
On Windows:
./.venv/Scripts/activate
Now let install necessary python grpc packages:
pip install grpcio
pip install grpcio-tools
Generate gRPC plumbing code for the Python project
With C# we had this nice nuget package which took care of generating gRPC plumbing code out of the contracts.proto
.
In Python, we’ll do it manually which still is pretty trivial
python -m grpc_tools.protoc -I../Shared --python_out=. --grpc_python_out=. contracts.proto
command line argument specifies where to find the proto
file (../Shared
), where to drop the generated files (.
that is, current directory) and, finally the name of the actual proto file.
After running this command you should notice contracts_pb2.py
and contracts_pb_grpc.py
files. These are the generated gRPC plumbing code, analogous what was generated into Shared/obj
in our C# project. So the workflow is pretty much the same as in C# project.
Client
We’ll start with the Python client first. The functionality of the Client and, subsequently, Service is going to mimic pretty much perfectly their C# analogs.
touch client.py
on top of this file, add the necessary imports. We’ll need os
and random
from python’s standard library, grpc
package and our generated modules
import random
import sys
import grpc
import contracts_pb2
import contracts_pb2_grpc
As in the C# project, the first argument is going to be the service host to connect to, and the second is going to be the service port. The following takes care of that:
# ...
# new
host = sys.argv[1]
port = int(sys.argv[2])
Next, we need to establish the connection to the service. As you recall we wanted to provide the Certificate Authority’s certificate to the connection so the client can verify service’s certificate upon connection. We are going to use the same certificates as in our C# exercise, so verify you still have them in the cert directory. The following sets up the connection as described:
# ...
with open("../cert/ca.pem", "rb") as f:
ca_cert = f.read()
creds = grpc.ssl_channel_credentials(root_certificates=ca_cert)
channel = grpc.secure_channel(f"{host}:{port}", creds)
Finally, create the client
# ...
client = contracts_pb2_grpc.SvcStub(channel)
We will want to demonstrate both basic RPC (calculator) and streaming RPC(time series) functionality.
so let’s add two placeholders for now, at the top of the client.py
, just below the imports
#...
def do_calculator(client):
pass
def do_time_series(client):
pass
At the bottom of the file, add our silly logic to dispatch either to do_calculator
or to do_time_series
, depending of how many arguments were provided.
# ...
if len(sys.argv) > 3:
do_calculator(client)
else:
do_time_series(client)
Let’s implement the calculator functionality first as it is a little bit simpler
def do_calculator(client):
x = int(sys.argv[3])
op = sys.argv[4]
y = int(sys.argv[5])
request = contracts_pb2.CalculateRequest(x=x, y=y, op=op)
reply = client.Calculate(request)
print(f"The result is {reply.result}")
Ok, let’s give it a try:
in the separate terminal window navigate to the root project’s directory and run the service
dotnet run -p Service 9000
Switch back to the python-specific terminal and invoke simple calculator request:
python client.py localhost 9000 17 + 25
Wow, our C# service calculates the requests coming from the python client!
Let’s see if we can do the time series exercise as well. As you remember, the client is supposed push the stream of temperature readings to the service, ad get the stream of medians back. We will use the same random walk strategy to generate the temperature readings.
Add this logic just above the do_time_series(client)
def generate_messages():
ts = 1
temp = 10
while True:
msg = contracts_pb2.Temperature(timestamp=ts, value=temp)
ts += 1
temp += random.random() - 0.5
yield msg
With this in place, replace the do_time_series
with:
def do_time_series(client):
for msg in client.Median(generate_messages()):
print(f"{msg.timestamp}: {msg.value}")
Make sure that the C# service is running and invoking the client triggering the time series logic:
python client.py localhost 9000
You’ll be seeing a stream of medians coming. Ctrl-C when you get bored.
The complete python client, for the reference
import random
import sys
import grpc
import contracts_pb2
import contracts_pb2_grpc
def do_calculator(client):
x = int(sys.argv[3])
op = sys.argv[4]
y = int(sys.argv[5])
request = contracts_pb2.CalculateRequest(x=x, y=y, op=op)
reply = client.Calculate(request)
print(f"The result is {reply.result}")
def generate_messages():
ts = 1
temp = 10
while True:
msg = contracts_pb2.Temperature(timestamp=ts, value=temp)
ts += 1
temp += random.random() - 0.5
yield msg
def do_time_series(client):
for msg in client.Median(generate_messages()):
print(f"{msg.timestamp}: {msg.value}")
host = sys.argv[1]
port = int(sys.argv[2])
with open("../cert/ca.pem", "rb") as f:
ca_cert = f.read()
creds = grpc.ssl_channel_credentials(root_certificates=ca_cert)
channel = grpc.secure_channel(f"{host}:{port}", creds)
client = contracts_pb2_grpc.SvcStub(channel)
if len(sys.argv) > 3:
do_calculator(client)
else:
do_time_series(client)
As you may observe, conceptually Python client is not that different from the C# client, modulo idioms specific to each language. The same can actually be said by the service implementation as well.
Service
We got the Python client working, let’s build the Python service.
touch service.py
Add the import se will need
from concurrent import futures
import statistics
import sys
import grpc
import contracts_pb2
import contracts_pb2_grpc
The service implementation should be pretty straightforward by now:
class Service(contracts_pb2_grpc.SvcServicer):
def Calculate(self, request, context):
result = -1
if request.op == "+":
result = request.x + request.y
elif request.op == "-":
result = request.x - request.y
elif request.op == "*":
result = request.x * request.y
elif request.op == "/":
if request.y != 0:
result = request.x // request.y
return contracts_pb2.CalculateReply(result=result)
def Median(self, request_iterator, context): # noqa
vals = []
for temp in request_iterator:
vals.append(temp.value)
med = 0
if len(vals) == 10:
med = statistics.median(vals)
vals = []
yield contracts_pb2.Temperature(timestamp=temp.timestamp, value=med)
The service will need to be provided with the certificate and private key
with open("../cert/service-key.pem", "rb") as f:
key = f.read()
with open("../cert/service.pem", "rb") as f:
cert = f.read()
creds = grpc.ssl_server_credentials(
[
(
key,
cert,
),
]
)
Once that is done, start it running:
port = int(sys.argv[1])
svc = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
contracts_pb2_grpc.add_SvcServicer_to_server(Service(), svc)
svc.add_secure_port(f"[::]:{port}", creds)
svc.start()
print(f"service listening on port {port}...")
svc.wait_for_termination()
Give it a try
python service.py 9010
Notice the different port so the C# service and Python service don’t conflict one with another. In a separate terminal, go to the root project directory and run the C# client, the calculator:
dotnet run -p Client localhost 9010 17 + 25
and the time series
dotnet run -p Client localhost 9010
Both will work fine. If you want, you can call python client to python service. Same result.
The complete Python service code
from concurrent import futures
import statistics
import sys
import grpc
import contracts_pb2
import contracts_pb2_grpc
class Service(contracts_pb2_grpc.SvcServicer):
def Calculate(self, request, context): # noqa
result = -1
if request.op == "+":
result = request.x + request.y
elif request.op == "-":
result = request.x - request.y
elif request.op == "*":
result = request.x * request.y
elif request.op == "/":
if request.y != 0:
result = request.x // request.y
return contracts_pb2.CalculateReply(result=result)
def Median(self, request_iterator, context): # noqa
vals = []
for temp in request_iterator:
vals.append(temp.value)
med = 0
if len(vals) == 10:
med = statistics.median(vals)
vals = []
yield contracts_pb2.Temperature(timestamp=temp.timestamp, value=med)
with open("../cert/service-key.pem", "rb") as f:
key = f.read()
with open("../cert/service.pem", "rb") as f:
cert = f.read()
creds = grpc.ssl_server_credentials(
[
(
key,
cert,
),
]
)
port = int(sys.argv[1])
svc = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
contracts_pb2_grpc.add_SvcServicer_to_server(Service(), svc)
svc.add_secure_port(f"[::]:{port}", creds)
svc.start()
print(f"service listening on port {port}...")
svc.wait_for_termination()
Interoperability between different languages is one of the most exciting gRPC features. You can implement the micro-service in the most suitable language and it will be a accessible from variety of clients.