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
- Philosophy
- Why Not Nameko or Others?
- Features
- Installation
- Quick Start
- Tutorials
- API Reference
- FAQ
- Changelog
- Contributing
- 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:
-
Install:
pip install noneapi
-
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()
-
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
-
Install:
pip install noneapi
-
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
noneapican communicate directly with this service by invoking theadd_ordermethod.-
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.
-
-
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_handlertoOrderServiceand establishPaymentServicewith anEventDispatcher. Whenever a 'payment_success' event is dispatched byPaymentService, theon_payment_successmethod inOrderServicewill 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 viaevent_handler. The parametersportandhostspecify where theEventDispatcherwill be available for dispatching events. -
ServiceProxy: A class offering access to remote services. The optional parametersevent_hostandevent_portdefine where the service listens for events. Because there are no message brokers, it's essential to know the publisher's location for event reception.
-
-
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 acceptscontainer,host, andportas arguments. Bothhostandportare optional. By default,hostis set to"*"andportto5555. -
run: A method that starts all the containers and the documentation service if applicable.
-
-
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
ClusterProxywith a configuration that points to theorder_service. TheClusterProxyis a context manager that allows us to access the service viacluster.order_service. Theadd_ordermethod 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, andport. Thenameis the identifier of the service. Thehostandportare the location of the service. Bothhostandportare optional. By default,hostis set to `"
-
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
ClusterProxywith a configuration that points to theorder_service. TheClusterProxyis a context manager that allows us to access the service viacluster.order_servicelike in the previous example but withasync_callthat allow us to call method asynchronously. Theresultmethod is invoked without arguments and returned result. -
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,boolandNonetypes are supported.If you want validate complex data, you can use
pydanticmodels 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
Ordermodel and return error if data is not valid. -
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/ -
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
29b5c956a051c0d9176ddfd87c4d63d0eb9111c2883f67cbb894a1938a6cd247
|
|
| MD5 |
0bfdee38e01a9b769b7cc8a63e7455d8
|
|
| BLAKE2b-256 |
11ac030476cd87f20439769fbed75d59af1aca87c06a653a300180c804f11129
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aa921fa2257bf7e8545a0dc311f97a3f52cfb8af7ec0b77dc1ef0ef85070850c
|
|
| MD5 |
62c33fd2833d3ff215e0fc08485c4f76
|
|
| BLAKE2b-256 |
cd16de7521294256cef1535d48a66d716e70860d773a42864d4b72d1e69a245e
|