Skip to main content

Micro RPC framework based on ZeroMQ for building distributed systems

Project description

NoneAPI (alpha)

This mini-framework (RPC like) allows you to easily build microservices that can communicate with each other like modules in a monolith. This project was inspired by the nameko framework.

🎯 Philosophy

Just call your microservices like you would any other function.

The goal of this framework is to simplify microservice interactions, eliminating the need for middleware. To send parameters like 'user' or anything else, include them in the method arguments. This ensures clarity in the data sent to and received from the service.

Performance: ~ 35000 requests per second on one worker


📜 Table of Contents

  1. Philosophy
  2. Why Not Nameko or Others?
  3. Features
  4. Installation
  5. Quick Start
  6. Tutorials
  7. API Reference
  8. FAQ
  9. Changelog
  10. Contributing
  11. License

❓ Why Not Nameko or Others?

We leverage ZeroMQ to eliminate broker intermediaries.


🌟 Features

  • Fast and reliable with ZeroMQ
  • Easy integration
  • Decoupled architecture
  • Event-driven design
  • Multi-subscriber support
  • No broker needed
  • Zero learning curve
  • Auto-generated documentation

⚙️ Installation

Install via pip:

pip install noneapi

Quick Start

Here's how to get started with MyLibrary:

  1. Install:

    pip install noneapi
    
  2. Create order service:

    # services.py
    from noneapi import rpc
    from noneapi import Container, ContainerRunner
    from .db import order_session
    from .models import Order
    
    
    class OrderService:
    
        name = 'order_service'
    
        @rpc
        def get_order(self, order_id: int):
            order = order_session.query(Order).get(order_id)
            if not order:
                return None
    
            return order.to_dict()
    
        @rpc
        def save_order(self, order: dict):
            order = Order(**order)
            order_session.add(order)
            order_session.commit()
            return order.to_dict()
    
    # containers.py
    container = Container(OrderService)
    runner = ContainerRunner(is_document_server=False)
    runner.register("order",container, host="*", port=5555)
    
    
    # app.py
    from .containers import runner
    
    if __name__ == '__main__':
        runner.run()
    
  3. Use by client:

    from noneapi import ClusterProxy
    from exceptions import RemoteError
    from .usecases import OrderUsecase
    config = [
        {
            "name": "order_service",
            "host": "127.0.0.1",
            "port": 5555
    
        }
    ]
    
    ...  # some other code
    
    with ClusterProxy(config) as cluster:
        order = cluster.order_service.get_order(1)
        order_usecase = OrderUsecase(order)
        updated_order = order_usecase.do_something()
        cluster.order_service.save_order.async_call(updated_order)
    
        ... # A lot of code
        
        try:
            result = cluster.order_service.save_order.result()
        except RemoteError as e:
            print(e)
            # do something
        else:
            # do something
    
        
    

Tutorials

  1. Install:

    pip install noneapi
    
  2. RPC:

    from noneapi import rpc
    
    
    class OrderService:
        name = 'order_service'
    
        @rpc
        def add_order(self, order: dict):
            # some code
            return order
    

    In this scenario, any service using noneapi can communicate directly with this service by invoking the add_order method.

    • name: Identifier for the service. This is used for service discovery and is mandatory for each service.

    • @rpc: A decorator that makes the method available for remote procedure calls. Only methods tagged with this decorator can be remotely invoked.

  3. Event handling

    from noneapi import rpc, event_handler, EventDispatcher, ServiceProxy
    
    class OrderService:
         name = 'order_service'
         payment_service = ServiceProxy(event_host="127.0.0.1", event_port=5556)
         
    
         @rpc
         def add_order(self, order: dict):
             # some code
             return order
         
         @event_handler(service_name='payment_service', topic='payment_success')
         def on_payment_success(self, order_id: int):
                 # some code
                 return order_id
    
    
    
    class PaymentService:
         name = 'payment_service'
         dispatch = EventDispatcher(port=5556, host='*')
         
         @rpc
         def pay(self, order_id: int):
             self.dispatch('payment_success', order_id)
             return order_id
    

    In this setup, we add an event_handler to OrderService and establish PaymentService with an EventDispatcher. Whenever a 'payment_success' event is dispatched by PaymentService, the on_payment_success method in OrderService will be triggered. Essentially, this mimics the publish/subscribe (pub/sub) pattern where you can subscribe to different topics.

    • @event_handler: A decorator used for methods that will be invoked when a specific event is dispatched. Only methods with this decorator respond to the event.

    • EventDispatcher: A class responsible for sending out events to all services that are listening via event_handler. The parameters port and host specify where the EventDispatcher will be available for dispatching events.

    • ServiceProxy: A class offering access to remote services. The optional parameters event_host and event_port define where the service listens for events. Because there are no message brokers, it's essential to know the publisher's location for event reception.

  4. Containers:

    from noneapi import Container, ContainerRunner
    from .services import OrderService
    
    container = Container(OrderService)
    runner = ContainerRunner(is_document_server=False)
    runner.register(container, host="*", port=5555)
    runner.run()
    

    In this case, we create a container with one service and run it.

    • Container: A class that holds all services that will be available for remote calls.

    • ContainerRunner: A class responsible for running all registered containers and facilitating service discovery.

    • register: A method to register a container within the runner. It accepts container, host, and port as arguments. Both host and port are optional. By default, host is set to "*" and port to 5555.

    • run: A method that starts all the containers and the documentation service if applicable.

  5. ClusterProxy:

    from noneapi import ClusterProxy
    from exceptions import RemoteError
    
    config = [
        {
            "name": "order_service",
            "host": "127.0.0.1",
            "port": 5555,
        }
    ]
    
    
    def main():
        with ClusterProxy(config) as cluster:
            order = cluster.order_service.add_order({"id": 1})
      
    

    In this example, we create a ClusterProxy with a configuration that points to the order_service. The ClusterProxy is a context manager that allows us to access the service via cluster.order_service. The add_order method is invoked with a dictionary as an argument.

    • ClusterProxy: A class that allows access to remote services. It accepts a configuration as an argument. The configuration is a list of dictionaries with the following keys: name, host, and port. The name is the identifier of the service. The host and port are the location of the service. Both host and port are optional. By default, host is set to `"
  6. Async call:

    from noneapi import ClusterProxy
    from exceptions import RemoteError
    
    config = [
        {
            "name": "order_service",
            "host": "127.0.0.1",
               "port": 5555,
        }
    ]
    
    def main():
        with ClusterProxy(config) as cluster:
            cluster.order_service.add_order.async_call({"id": 1})
            # a lot of code
            try:
                result = cluster.order_service.result()
            except RemoteError as e:
                print(e)
                # do something
            else:
                # do something
    

    In this example, we create a ClusterProxy with a configuration that points to the order_service. The ClusterProxy is a context manager that allows us to access the service via cluster.order_service like in the previous example but with async_call that allow us to call method asynchronously. The result method is invoked without arguments and returned result.

  7. Validation with pydantic

    from noneapi import rpc 
    
    class OrderService:
         name = 'order_service'
    
         @rpc
         def add_order(self, order_id: int, name: str):
             # some code
             return order
    

    In this scenario, we can validate input. NoneAPI will validate input data and return error if data is not valid. Only int, float, str, bool and None types are supported.

    If you want validate complex data, you can use pydantic models for that:

    from noneapi import rpc
    from pydantic import BaseModel
    
    class Order(BaseModel):
        id: int
        name: str
    
    class OrderService:
        name = 'order_service'
    
        @rpc
        def add_order(self, order: Order):
            # some code
            return order
    

    In this case NoneAPI will validate input data with Order model and return error if data is not valid.

  8. Docs

    from noneapi import Container, ContainerRunner
    from .services import OrderService
    
    container = Container(OrderService)
    runner = ContainerRunner(is_document_server=True)
    runner.register(container, host="*", port=5555)
    runner.run()
    

    In this case, we create a container with one service and run it with documentation server. It will be always available on http://localhost:8081/

  9. Custom serialization

    from noneapi import BaseSerializer
    from noneapi import Protocol
    from msgpack import packb, unpackb
    
    class MsgPackSerializer(BaseSerializer):
        def _serialize(self, data: dict) -> bytes:
            return packb(data)
    
        def _deserialize(self, data: bytes) -> dict:
            return unpackb(data)
    
    class OrderService:
        name = 'order_service'
        protocol: Protocol[MsgPackSerializer] = Protocol(MsgPackSerializer())
    
        @rpc
        def add_order(self, order: dict):
            # some code
            return order
    

    In this case, we're creating a custom serializer for the service, applicable to all its methods. By default, NoneAPI employs a clean JSON serializer, powered by the ultra-fast orjson library. Feel free to use any serializer—just inherit from BaseSerializer and pass it to the Protocol class.

    IMPORTANT: If you change the serializer, you must change it on all services that will communicate with each other. Otherwise, you will get an error.


Changelog

Version 0.1.3 (2023-10-29)(alpha)

  • Initial release

Version 0.1.4 (2023-10-29)(alpha)

  • Add custom serializer
  • Add custom serializer documentation

Contributing

To contribute, please fork the repository, make your changes, and submit a pull request.


License

This project is licensed under the MIT License. See the LICENSE.md file for details.

If you like this project, please give it a star! 🌟

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

noneapi-0.1.5.tar.gz (21.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

noneapi-0.1.5-py3-none-any.whl (19.2 kB view details)

Uploaded Python 3

File details

Details for the file noneapi-0.1.5.tar.gz.

File metadata

  • Download URL: noneapi-0.1.5.tar.gz
  • Upload date:
  • Size: 21.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.9

File hashes

Hashes for noneapi-0.1.5.tar.gz
Algorithm Hash digest
SHA256 29b5c956a051c0d9176ddfd87c4d63d0eb9111c2883f67cbb894a1938a6cd247
MD5 0bfdee38e01a9b769b7cc8a63e7455d8
BLAKE2b-256 11ac030476cd87f20439769fbed75d59af1aca87c06a653a300180c804f11129

See more details on using hashes here.

File details

Details for the file noneapi-0.1.5-py3-none-any.whl.

File metadata

  • Download URL: noneapi-0.1.5-py3-none-any.whl
  • Upload date:
  • Size: 19.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.9

File hashes

Hashes for noneapi-0.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 aa921fa2257bf7e8545a0dc311f97a3f52cfb8af7ec0b77dc1ef0ef85070850c
MD5 62c33fd2833d3ff215e0fc08485c4f76
BLAKE2b-256 cd16de7521294256cef1535d48a66d716e70860d773a42864d4b72d1e69a245e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page