Let's make the canonical Twirp service: a Haberdasher
.
The Haberdasher
service makes hats. It has only one RPC method, MakeHat
,
which makes a new hat of a particular size.
Make sure to Install Protobuf and Twirp before starting.
By the end of this, we'll run a Haberdasher service with a strongly typed client.
There are 5 steps here:
- Write a Protobuf service definition
- Generate code
- Implement the server
- Mount and run the server
- Use the client
Start with the proto definition file, placed in rpc/haberdasher/service.proto
:
syntax = "proto3";
package twirp.example.haberdasher;
option go_package = "github.com/example/rpc/haberdasher";
// Haberdasher service makes hats for clients.
service Haberdasher {
// MakeHat produces a hat of mysterious, randomly-selected color!
rpc MakeHat(Size) returns (Hat);
}
// Size of a Hat, in inches.
message Size {
int32 inches = 1; // must be > 0
}
// A Hat is a piece of headwear made by a Haberdasher.
message Hat {
int32 inches = 1;
string color = 2; // anything but "invisible"
string name = 3; // i.e. "bowler"
}
It's a good idea to add comments on your Protobuf file. These files can work as the primary documentation of your API. The comments also show up in the generated Go types.
To generate code run the protoc
compiler pointed at your service's .proto
files:
$ protoc --python_out=. --pyi_out=. --twirpy_out=. \
example/rpc/haberdasher/service.proto
The code should be generated in the same directory as the .proto
files.
/example # your python module root
__init__.py
/client
...
/server
...
/rpc
/haberdasher
service.proto
service_pb2.py # generated by protoc
service_pb2.pyi # generated by protoc
service_twirp.py # generated by protoc-gen-twirpy
If you open the generated service_twirp.py
file, you should see a Python protocol like this:
class HaberdasherServiceProtocol(Protocol):
def MakeHat(self, ctx: Context, request: _haberdasher_pb2.Size) -> _haberdasher_pb2.Hat: ...
along with code to instantiate clients and servers.
Now, our job is to write code that fulfills the HaberdasherServiceProtocol
protocol. This
will be the "backend" logic to handle the requests.
The implementation could go in example/server/services.py
:
import random
from twirp.context import Context
from twirp.exceptions import InvalidArgument
from ..rpc.haberdasher import service_pb2 as pb
class HaberdasherService:
def MakeHat(self, context: Context, size: pb.Size) -> pb.Hat:
if size.inches <= 0:
raise InvalidArgument(argument="inches", error="I can't make a hat that small!")
return pb.Hat(
size=size.inches,
color=random.choice(["white", "black", "brown", "red", "blue"]),
name=random.choice(["bowler", "baseball cap", "top hat", "derby"])
)
To serve our Haberdasher over HTTP, use the generated server class {{Service}}Server
.
For Haberdasher, it is: class HaberdasherServer(TwirpServer)
.
This constructor wraps your protocol implementation as a TwirpServer
, which needs to be added as a service to a TwirpASGIApp
.
In example/server/__init__.py
:
from twirp.asgi import TwirpASGIApp
from ..rpc.haberdasher.service_twirp import HaberdasherServer
from .services import HaberdasherService
service = HaberdasherServer(service=HaberdasherService())
app = TwirpASGIApp()
app.add_service(service)
You will need to install uvicorn
to run the example.
pip install uvicorn
If you run uvicorn example.server:app --port=8080
, you'll be running your server at localhost:8080
.
All that's left is to create a client!
Client stubs are automatically generated, hooray!
For each service, there are 2 client classes:
{{Service}}Client
for synchronous clients usingrequests
.Async{{Service}}Client
for asynchronous clients usingaiohttp
.
Clients in other languages can also be generated by using the respective protoc
plugins defined by their languages.
For example, in example/client/__main__.py
:
from twirp.context import Context
from twirp.exceptions import TwirpServerException
from ..rpc.haberdasher import service_pb2 as pb
from ..rpc.haberdasher.service_twirp import HaberdasherClient
def main():
client = HaberdasherClient("http://localhost:8080")
try:
response = client.MakeHat(
ctx=Context(),
request=pb.Size(inches=12),
)
print(f"I have a nice new hat:\n{response}")
except TwirpServerException as e:
print(e.code, e.message, e.meta, e.to_dict())
if __name__ == "__main__":
main()
If you have the server running in another terminal, try running this client with python -m example.client
.
Enjoy the new hat!
You can also make an asynchronous version of it.
For example, in example/client/async.py
:
import asyncio
import aiohttp
from twirp.context import Context
from twirp.exceptions import TwirpServerException
from ..rpc.haberdasher import service_pb2 as pb
from ..rpc.haberdasher.service_twirp import AsyncHaberdasherClient
async def main():
server_url = "http://localhost:8080"
async with aiohttp.ClientSession(server_url) as session:
client = AsyncHaberdasherClient(server_url, session=session)
try:
response = await client.MakeHat(
ctx=Context(),
request=pb.Size(inches=12),
)
print(f"I have a nice new hat:\n{response}")
except TwirpServerException as e:
print(e.code, e.message, e.meta, e.to_dict())
if __name__ == "__main__":
asyncio.run(main())
You will need to install aiohttp
to run the example.
pip install twirp[async]
Try running this client with python -m example.client.async
.