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.protoThe 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_async_client_twirp.py # generated by protoc-gen-twirpy
service_client_twirp.py # generated by protoc-gen-twirpy
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 the code to instantiate the server, and a Python protocol like this:
class HaberdasherServiceProtocol(Protocol):
async def MakeHat(self, ctx: Context, request: _haberdasher_pb2.Size) -> _haberdasher_pb2.Hat: ...If you open service_client_twirp.py and service_async_client_twirp.py, you will see the client code for the clients.
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:
async 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 uvicornIf 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}}Clientfor synchronous clients usingrequests.Async{{Service}}Clientfor 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_client_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_async_client_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.