Skip to main content

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 or raw TCP 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.
  • Built-in support for Pydantic.
  • 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.

Documentation 📚

The documentation can be found here.

Getting started 🚀

Ensure Python 3.9+

pip install zeroapi
pip install "zeroapi[uvloop]"  # for better async performance on linux and mac-os
pip install "zeroapi[pydantic]"  # for pydantic support
pip install "zeroapi[tornado]"  # for windows async support
pip install "zeroapi[all]"  # for all extras

Basic example

  • 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())
    

TCP client/server

  • By default Zero uses ZeroMQ for communication. But if you want to use raw TCP, you can use the protocol parameter.

    from zero import ZeroServer
    from zero.protocols.tcp import TCPServer
    
    app = ZeroServer(port=5559, protocol=TCPServer)  # <-- Note the protocol parameter
    
    @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()
    
  • In that case the client should also use TCP protocol.

    import asyncio
    
    from zero import AsyncZeroClient
    from zero import ZeroClient
    from zero.protocols.tcp import AsyncTCPClient
    
    zero_client = ZeroClient("localhost", 5559, protocol=AsyncTCPClient)  # <-- Note the protocol parameter
    
    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())
    

TCP has better performance and throughput than ZeroMQ. We might make it the default protocol in future releases.

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) -> bool:
    # save person to db
    ...

@app.register_rpc
def save_order(order: Order) -> bool:
    # save order to db
    ...

Pydantic support

Pydantic models are also supported out of the box. Just use pydantic.BaseModel as the argument or return type and install zero with pydantic extra.

pip install zeroapi[pydantic]

Custom serializer

If you want to use a custom serializer, you can create your own serializer by implementing the Encoder interface.

class MyCustomEncoder(Encoder):
    def encode(self, obj: Any) -> bytes:
        # implement your custom serialization logic here
        ...

    def decode(self, data: bytes, type_hint: Type[Any]) -> Any:
        # implement your custom deserialization logic here
        ...

Then pass the serializer to both* server and client.

from zero import ZeroServer, ZeroClient
from my_custom_encoder import MyCustomEncoder

app = ZeroServer(port=5559, encoder=MyCustomEncoder)
zero_client = ZeroClient("localhost", 5559, encoder=MyCustomEncoder)

Return type on client

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 with schema support!

  • After running the server, like above, you can generate client code using the zero.generate_client module.

    This makes it easy to get the latest schemas on live servers and not to maintain other file sharing approach to manage schemas.

    Using zero.generate_client generate client code for even remote servers using the --host, --port, and --protocol options.

    python -m zero.generate_client --host localhost --port 5559 --protocol zmq --overwrite-dir ./my_client
    
  • It will generate client like this -

    from dataclasses import dataclass
    from msgspec import Struct
    from datetime import datetime
    
    from zero import ZeroClient
    
    
    zero_client = ZeroClient("localhost", 5559)
    
    class Person(Struct):
        name: str
        age: int
        dob: datetime
    
    
    @dataclass
    class Order:
        id: int
        amount: float
        created_at: datetime
    
    
    class RpcClient:
        def __init__(self, zero_client: ZeroClient):
            self._zero_client = zero_client
    
        def save_person(self, person: Person) -> bool:
            return self._zero_client.call("save_person", person)
    
        def save_order(self, order: Order) -> bool:
            return self._zero_client.call("save_order", order)
    

    Check the schemas are copied!

  • Use the client -

    from my_client import RpcClient, zero_client
    
    client = RpcClient(zero_client)
    
    if __name__ == "__main__":
        client.save_person(Person(name="John", age=25, dob=datetime.now()))
        client.save_order(Order(id=1, amount=100.0, created_at=datetime.now()))
    

Async client code generation

  • To generate async client code, use the --async flag.

    python -m zero.generate_client --host localhost --port 5559 --protocol zmq --overwrite-dir ./my_async_client --async
    

*tcp protocol will always generate async client.

Important notes! 📝

For multiprocessing

  • ZeroServer should always be run under if __name__ == "__main__":, as it uses multiprocessing.
  • ZeroServer creates the workers in different processes, so anything global in your code will be instantiated N times where N is the number of workers. So if you want to initiate them once, put them under if __name__ == "__main__":. But recommended to not use global vars. And Databases, Redis, other clients, creating them N times in different processes is fine and preferred.

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 🏆

13th Gen Intel® Core™ i9-13900HK @ 5.40GHz, 14 cores, 20 threads, 32GB 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 33167.69 11.89 17959.46 12.76
aiozmq 25174.24 6.13 8850.15 10.19
blacksheep 38025.53 8.41 16324.19 13.54
fastApi 19682.99 9.09 12775.97 16.28
sanic 58811.27 4.43 23622.69 9.22
zero(sync) 27570.85 6.65 10269.1 23.71
zero(async) 41091.96 4.41 23996.18 8.64
zero(tcp) 100752.12 2.33 35812.88 13.48

Contribution

Contributors are welcomed 🙏

Please leave a star ⭐ if you like Zero!

"Buy Me A Coffee"

Release files for zeroapi 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for zeroapi 1.0.1
File Size Uploaded
zeroapi-1.0.1.tar.gz 52.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for zeroapi 1.0.1
File Interpreter ABI Platform
zeroapi-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 122.3 kB

Release files / zeroapi-1.0.1.tar.gz

Download URL zeroapi-1.0.1.tar.gz
Size 52.9 kB
Tags Source
SHA-256 checksum
How to use checksums
981be16fa45b667abfeb65cddb3cc2f5315c29e48eb164e05e8adb99881a4208
BLAKE2b-256 checksum
How to use checksums
d165e2739d0f21093dc6eea12d59dea1d1db09e0c5cf7280fee6c513b8bdaec5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.3

Release files / zeroapi-1.0.1-py3-none-any.whl

Download URL zeroapi-1.0.1-py3-none-any.whl
Size 69.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
356f3d395c6a307bdcd3dc48f8d0313d2ef763d0a0b91ba587064d4fa781a08f
BLAKE2b-256 checksum
How to use checksums
62070b76fe5bc8656b73a29e02d45ef22b550f9f43581b43f42779a69e8128eb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

1.0.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page