Zero is a simple Python framework (RPC like) to build fast and high performance microservices or distributed servers
Features:
- Zero provides faster communication (see benchmarks) between the microservices using zeromq under the hood.
- Zero uses messages for communication and traditional client-server or request-reply pattern is supported.
- Support for both async and sync.
- The base server (ZeroServer) utilizes all cpu cores.
- Code generation! See example ๐
Philosophy behind Zero:
- Zero learning curve: The learning curve is tends to zero. Just add functions and spin up a server, literally that's it! The framework hides the complexity of messaging pattern that enables faster communication.
- ZeroMQ: An awesome messaging library enables the power of Zero.
Let's get started!
Getting started ๐
Ensure Python 3.8+
pip install zeroapi
For Windows, tornado needs to be installed separately (for async operations). It's not included with zeroapi
because for linux and mac-os, tornado is not needed as they have their own event loops.
-
Create a
server.py
from zero import ZeroServer app = ZeroServer(port=5559) @app.register_rpc def echo(msg: str) -> str: return msg @app.register_rpc async def hello_world() -> str: return "hello world" if __name__ == "__main__": app.run()
-
The RPC functions only support one argument (
msg
) for now. -
Also note that server RPC functions are type hinted. Type hint is must in Zero server. Supported types can be found here.
-
Run the server
python -m server
-
Call the rpc methods
from zero import ZeroClient zero_client = ZeroClient("localhost", 5559) def echo(): resp = zero_client.call("echo", "Hi there!") print(resp) def hello(): resp = zero_client.call("hello_world", None) print(resp) if __name__ == "__main__": echo() hello()
-
Or using async client -
import asyncio from zero import AsyncZeroClient zero_client = AsyncZeroClient("localhost", 5559) async def echo(): resp = await zero_client.call("echo", "Hi there!") print(resp) async def hello(): resp = await zero_client.call("hello_world", None) print(resp) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(echo()) loop.run_until_complete(hello())
Serialization ๐ฆ
Default serializer
Msgspec is the default serializer. So msgspec.Struct
(for high performance) or dataclass
or any supported types can be used easily to pass complex arguments, i.e.
from dataclasses import dataclass
from msgspec import Struct
from zero import ZeroServer
app = ZeroServer()
class Person(Struct):
name: str
age: int
dob: datetime
@dataclass
class Order:
id: int
amount: float
created_at: datetime
@app.register_rpc
def save_person(person: Person) -> None:
# save person to db
...
@app.register_rpc
def save_order(order: Order) -> None:
# save order to db
...
Return type
The return type of the RPC function can be any of the supported types. If return_type
is set in the client call
method, then the return type will be converted to that type.
@dataclass
class Order:
id: int
amount: float
created_at: datetime
def get_order(id: str) -> Order:
return zero_client.call("get_order", id, return_type=Order)
Code Generation ๐ค
Easy to use code generation tool is also provided!
After running the server, like above, call the server to get the client code. This makes it easy to know what functions are available in the local or remote server.
Using zero.generate_client
generate client code for even remote servers using the --host
and --port
options.
python -m zero.generate_client --host localhost --port 5559 --overwrite-dir ./my_client
It will generate client like this -
import typing # remove this if not needed
from typing import List, Dict, Union, Optional, Tuple # remove this if not needed
from zero import ZeroClient
zero_client = ZeroClient("localhost", 5559)
class RpcClient:
def __init__(self, zero_client: ZeroClient):
self._zero_client = zero_client
def echo(self, msg: str) -> str:
return self._zero_client.call("echo", msg)
def hello_world(self, msg: str) -> str:
return self._zero_client.call("hello_world", msg)
Use the client -
from my_client import RpcClient, zero_client
client = RpcClient(zero_client)
if __name__ == "__main__":
client.echo("Hi there!")
client.hello_world(None)
Currently, the code generation tool supports only ZeroClient
and not AsyncZeroClient
.
WIP - Generate models from server code.
Important notes ๐
ZeroServer
should always be run underif __name__ == "__main__":
, as it uses multiprocessing.- The methods which are under
register_rpc()
inZeroServer
should have type hinting, likedef echo(msg: str) -> str:
Let's do some benchmarking! ๐
Zero is all about inter service communication. In most real life scenarios, we need to call another microservice.
So we will be testing a gateway calling another server for some data. Check the benchmark/dockerize folder for details.
There are two endpoints in every tests,
/hello
: Just call for a hello world response ๐/order
: Save a Order object in redis
Compare the results! ๐
Benchmarks ๐
11th Gen Intelยฎ Coreโข i7-11800H @ 2.30GHz, 8 cores, 16 threads, 16GB RAM (Docker in Ubuntu 22.04.2 LTS)
(Sorted alphabetically)
Framework | "hello world" (req/s) | 99% latency (ms) | redis save (req/s) | 99% latency (ms) |
---|---|---|---|---|
aiohttp | 14391.38 | 10.96 | 9470.74 | 12.94 |
aiozmq | 15121.86 | 9.42 | 5904.84 | 21.57 |
fastApi | 9590.96 | 18.31 | 6669.81 | 24.41 |
sanic | 18790.49 | 8.69 | 12259.29 | 13.52 |
zero(sync) | 24805.61 | 4.57 | 16498.83 | 7.80 |
zero(async) | 22716.84 | 5.61 | 17446.19 | 7.24 |
Roadmap ๐บ
- Make msgspec as default serializer
- Add support for async server (currently the sync server runs async functions in the eventloop, which is blocking)
- Add pub/sub support
Contribution
Contributors are welcomed ๐
Please leave a star โญ if you like Zero!