Metadata-Version: 2.1
Name: noneapi
Version: 0.1.1
Summary: Small RPC framework based on ZeroMQ
Author-email: Denys Rozlomii <eighty.eighth.inc@gmail.com>
License: MIT
Project-URL: repository, https://github.com/EightyEighth/noneapi
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Description-Content-Type: text/markdown
License-File: LICENSE.md
Requires-Dist: pyzmq==25.1.1
Requires-Dist: loguru==0.7.2
Requires-Dist: gevent==23.9.1
Requires-Dist: pdoc==14.1.0
Requires-Dist: pydantic==2.4.2
Requires-Dist: orjson==3.9.10
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: flake8; extra == "dev"
Requires-Dist: black; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: isort; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: requests; extra == "dev"

# 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](https://github.com/nameko/nameko).

## 🎯 Philosophy

> Just call your microservices like you would any other function.

The goal of this framework is to simplify microservice interactions—no middleware necessary.


Performance:
~ 35000 requests per second on one worker

---

## 📜 Table of Contents

1. [Philosophy](#philosophy)
2. [Why Not Nameko or Others?](#why-not-nameko-or-others)
3. [Features](#features)
4. [Installation](#installation)
5. [Quick Start](#quick-start)
6. [Tutorials](#tutorials)
7. [API Reference](#api-reference)
8. [FAQ](#faq)
9. [Changelog](#changelog)
10. [Contributing](#contributing)
11. [License](#license)

---

## ❓ Why Not Nameko or Others?

We leverage [ZeroMQ](https://zeromq.org/) 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:

```bash
pip install noneapi
````

## Quick Start
Here's how to get started with MyLibrary:

1. **Install**: 
    ```bash
    pip install noneapi
    ```

2. **Create order service**: 
    ```python
    # 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(container, host="*", port=5555)
   
   
   # app.py
    from .containers import runner
   
    if __name__ == '__main__':
        runner.run()
    ```
3. **Use by client**: 
    ```python
   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**: 
    ```bash
    pip install zero-connect
    ```

2. **RPC**: 
    ```python
    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 `zero-connect` 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**

   ```python
   
   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**: 
    ```python
    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**: 
    ```python
    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})
            print(order)
            try:
                result = cluster.order_service.add_order({"id": 1})
            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`. The `add_order` method is invoked with a dictionary as an argument. The result is then printed to the console. If an error occurs, the `RemoteError` exception is raised.

    - **`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**: 
    ```python
    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. **Docs**
    ```python
    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/`

---

## Changelog

### Version 0.1.0 (2023-10-29)(alpha)
- Initial release

---

## 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! 🌟
