Skip to content

Latest commit

 

History

History
222 lines (159 loc) · 6.17 KB

README.md

File metadata and controls

222 lines (159 loc) · 6.17 KB

Twirp Python Example

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:

  1. Write a Protobuf service definition
  2. Generate code
  3. Implement the server
  4. Mount and run the server
  5. Use the client

Write a Protobuf Service Definition

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.

Generate code

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.

Implement the Server

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"])
        )

Mount and run the server

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!

Use the Client

Client stubs are automatically generated, hooray!

For each service, there are 2 client classes:

  • {{Service}}Client for synchronous clients using requests.
  • Async{{Service}}Client for asynchronous clients using aiohttp.

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.