diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 0000000..e7e29e5 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,26 @@ +name: Pylint + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install requests + pip install flask + pip install fastapi + pip install pylint + - name: Analysing the code with pylint + run: | + pylint $(git ls-files '*.py') diff --git a/oborpc/base/__init__.py b/oborpc/base/__init__.py index e69de29..234f254 100644 --- a/oborpc/base/__init__.py +++ b/oborpc/base/__init__.py @@ -0,0 +1,3 @@ +""" +OBORPC Bae +""" diff --git a/oborpc/base/meta.py b/oborpc/base/meta.py index 615952e..818fcff 100644 --- a/oborpc/base/meta.py +++ b/oborpc/base/meta.py @@ -7,8 +7,8 @@ class OBORMeta(type): Meta class used """ __obor_registry__ = {} - def __new__(mcls, name, bases, namespace, /, **kwargs): - cls = super().__new__(mcls, name, bases, namespace, **kwargs) + def __new__(mcs, name, bases, namespace, /, **kwargs): + cls = super().__new__(mcs, name, bases, namespace, **kwargs) cls.__oborprocedures__ = { methodname for methodname, value in namespace.items() @@ -19,7 +19,12 @@ def __new__(mcls, name, bases, namespace, /, **kwargs): return cls -class OBORBase(metaclass=OBORMeta): +class OBORBase(metaclass=OBORMeta): # pylint: disable=too-few-public-methods """ Obor Base Class """ + def __repr__(self) -> str: + return "" + + def __str__(self) -> str: + return self.__repr__() diff --git a/oborpc/builder/__init__.py b/oborpc/builder/__init__.py index 9d564c3..c7831e1 100644 --- a/oborpc/builder/__init__.py +++ b/oborpc/builder/__init__.py @@ -1,5 +1,9 @@ +""" +OBORPC Builder +""" + from ._base import OBORBuilder from ._client import ClientBuilder from ._server import ServerBuilder from ._fastapi import FastAPIServerBuilder -from ._flask import FlaskServerBuilder \ No newline at end of file +from ._flask import FlaskServerBuilder diff --git a/oborpc/builder/_base.py b/oborpc/builder/_base.py index c30d9fe..bc8b8c7 100644 --- a/oborpc/builder/_base.py +++ b/oborpc/builder/_base.py @@ -1,7 +1,12 @@ """ +OBORPC Base Builder """ +from ..exception import OBORPCBuildException class OBORBuilder(): + """ + OBORPC Builder Class + """ __registered_base = set() def __init__(self, host, port=None, timeout=1, retry=0) -> None: @@ -20,6 +25,9 @@ def __init__(self, host, port=None, timeout=1, retry=0) -> None: self.base_url += f":{port}" def check_has_protocol(self, host: str): + """ + Check whether the given host already defined with protocol or not + """ if host.startswith("http://"): return True if host.startswith("https://"): @@ -27,6 +35,10 @@ def check_has_protocol(self, host: str): return False def check_registered_base(self, base: str): + """ + Check whether the base RPC class is already built + """ if base in OBORBuilder.__registered_base: - raise Exception(f"Failed to build client RPC {base} : base class can only built once") + msg = f"Failed to build client RPC {base} : base class can only built once" + raise OBORPCBuildException(msg) OBORBuilder.__registered_base.add(base) diff --git a/oborpc/builder/_client.py b/oborpc/builder/_client.py index b87c9bc..58b625d 100644 --- a/oborpc/builder/_client.py +++ b/oborpc/builder/_client.py @@ -1,20 +1,36 @@ """ +Client RPC Builder """ import inspect import json -import requests +import logging import time +import requests from ._base import OBORBuilder +from ..exception import RPCCallException class ClientBuilder(OBORBuilder): - def __init__(self, host, port=None, timeout=1, retry=0) -> None: - super().__init__(host, port, timeout, retry) - - def create_remote_caller(self, class_name, method_name, url_prefix, timeout = None, retry = None): + """ + Client Builder + """ + def create_remote_caller( + self, + class_name: str, + method_name: str, + url_prefix: str, + timeout: float = None, + retry: int = None + ): # pylint: disable=too-many-arguments + """ + create remote caller + """ def remote_call(*args, **kwargs): + """ + remote call wrapper + """ + start_time = time.time() try: - t0 = time.time() data = { "args": args[1:], "kwargs": kwargs @@ -24,28 +40,41 @@ def remote_call(*args, **kwargs): url, headers={"Content-Type": "application/json"}, json=json.dumps(data), - timeout=timeout if timeout != None else self.timeout + timeout=timeout if timeout is not None else self.timeout ) + if not response: - raise Exception(f"rpc call failed method={method_name}") + msg = f"rpc call failed method={method_name}" + raise RPCCallException(msg) + return response.json().get("data") + except Exception as e: - _retry = retry if retry != None else self.retry + _retry = retry if retry is not None else self.retry if _retry: return remote_call(*args, **kwargs, retry=_retry-1) - raise Exception(f"rpc call failed method={method_name}") + + if isinstance(e, RPCCallException): + raise e + msg = f"rpc call failed method={method_name} : {e}" + raise RPCCallException(msg) from e + finally: - # print("elapsed", time.time() - t0) - pass + elapsed = f"{(time.time() - start_time) * 1000}:.2f" + logging.debug("[RPC-Clientt] remote call take %s ms", elapsed) + return remote_call - def setup_client_rpc(self, instance: object, url_prefix: str = ""): + def build_client_rpc(self, instance: object, url_prefix: str = ""): + """ + Setup client rpc + """ _class = instance.__class__ iterator_class = _class self.check_registered_base(_class) - for (name, method) in inspect.getmembers(iterator_class, predicate=inspect.isfunction): + for (name, _) in inspect.getmembers(iterator_class, predicate=inspect.isfunction): if name not in iterator_class.__oborprocedures__: continue setattr(_class, name, self.create_remote_caller(_class.__name__, name, url_prefix)) diff --git a/oborpc/builder/_fastapi.py b/oborpc/builder/_fastapi.py index 42a40f1..85870b3 100644 --- a/oborpc/builder/_fastapi.py +++ b/oborpc/builder/_fastapi.py @@ -1,19 +1,21 @@ """ +FastAPI Server Builder """ import json import asyncio from enum import Enum +from typing import Optional, List, Dict, Union, Type, Any, Sequence, Callable from fastapi import Request, Response, APIRouter, params from fastapi.responses import JSONResponse from fastapi.routing import BaseRoute, APIRoute, ASGIApp, Lifespan, Default, generate_unique_id -from typing import Optional, List, Dict, Union, Type, Any, Sequence, Callable from ._server import ServerBuilder from ..base.meta import OBORBase -class FastAPIServerBuilder(ServerBuilder): - def __init__(self, host, port=None, timeout=1, retry=None): - super().__init__(host, port, timeout, retry) +class FastAPIServerBuilder(ServerBuilder): + """ + Dedicated RPC Server Builder for FastAPI + """ def create_remote_responder( self, instance: OBORBase, @@ -21,7 +23,7 @@ def create_remote_responder( class_name: str, method_name: str, method: Callable - ): + ): # pylint: disable=too-many-arguments @router.post(f"{router.prefix}/{class_name}/{method_name}") def final_func(request: Request): request_body = asyncio.run(request.body()) @@ -49,8 +51,9 @@ def build_router_from_instance( deprecated: Optional[bool] = None, include_in_schema: bool = True, generate_unique_id_function: Callable[[APIRoute], str] = Default(generate_unique_id), - ): + ): # pylint: disable=too-many-arguments,too-many-locals """ + build FastAPI API Router from oborpc instance """ router = APIRouter( prefix=prefix, diff --git a/oborpc/builder/_flask.py b/oborpc/builder/_flask.py index 4d3d8ff..fe7d948 100644 --- a/oborpc/builder/_flask.py +++ b/oborpc/builder/_flask.py @@ -1,19 +1,26 @@ """ +Flask Server Builder """ import functools import json import os +from typing import Callable, Union from flask import request as flask_request, Blueprint from ._server import ServerBuilder from ..base.meta import OBORBase class FlaskServerBuilder(ServerBuilder): - def __init__(self, host, port=None, timeout=1, retry=None): - super().__init__(host, port, timeout, retry) - + """ + Dedicated RPC Server Builder for Flask + """ def create_remote_responder( - self, instance: OBORBase, router: Blueprint, class_name, method_name, method - ): + self, + instance: OBORBase, + router: Blueprint, + class_name: str, + method_name: str, + method: Callable + ): # pylint: disable=too-many-arguments def create_modified_func(): @functools.wraps(method) def modified_func(): @@ -31,16 +38,17 @@ def build_blueprint_from_instance( instance: OBORBase, blueprint_name: str, import_name: str, - static_folder: str | os.PathLike | None = None, - static_url_path: str | None = None, - template_folder: str | os.PathLike | None = None, - url_prefix: str | None = None, - subdomain: str | None = None, - url_defaults: dict | None = None, - root_path: str | None = None, - cli_group: str | None = object() - ): + static_folder: Union[str, os.PathLike, None] = None, + static_url_path: Union[str, None] = None, + template_folder: Union[str, os.PathLike, None] = None, + url_prefix: Union[str, None] = None, + subdomain: Union[str, None] = None, + url_defaults: Union[dict, None] = None, + root_path: Union[str, None] = None, + cli_group: Union[str, None] = object() + ): # pylint: disable=too-many-arguments """ + build Flask blueprint from oborpc instance """ blueprint = Blueprint( blueprint_name, diff --git a/oborpc/builder/_server.py b/oborpc/builder/_server.py index cf15487..3a3c304 100644 --- a/oborpc/builder/_server.py +++ b/oborpc/builder/_server.py @@ -1,26 +1,36 @@ """ +Server Builder Base """ import inspect from ._base import OBORBuilder class ServerBuilder(OBORBuilder): - def __init__(self, host, port=None, timeout=1, retry=0) -> None: - super().__init__(host, port, timeout, retry) - - def create_remote_responder(self, instance, router, class_name, method_name, method): + """ + Server Builder + """ + def create_remote_responder(self, instance, router, class_name, method_name, method): # pylint: disable=too-many-arguments + """ + Remote RPC Request Responder + """ raise NotImplementedError("method should be overridden") def dispatch_rpc_request(self, instance, method, body): + """ + Dispatch RPC Request + """ args = body.get("args", []) kwargs = body.get("kwargs", {}) res = method(instance, *args, **kwargs) return {"data": res} def setup_server_rpc(self, instance: object, router): + """ + Setup RPC Server + """ _class = instance.__class__ iterator_class = instance.__class__.__base__ - method_map = { + method_map = { # pylint: disable=unnecessary-comprehension name: method for (name, method) in inspect.getmembers( _class, predicate=inspect.isfunction ) @@ -29,4 +39,7 @@ def setup_server_rpc(self, instance: object, router): for (name, method) in inspect.getmembers(iterator_class, predicate=inspect.isfunction): if name not in iterator_class.__oborprocedures__: continue - self.create_remote_responder(instance, router, iterator_class.__name__, name, method_map.get(name)) + self.create_remote_responder( + instance, router, iterator_class.__name__, + name, method_map.get(name) + ) diff --git a/oborpc/decorator.py b/oborpc/decorator.py index b17ad2f..30a4c07 100644 --- a/oborpc/decorator.py +++ b/oborpc/decorator.py @@ -1,8 +1,28 @@ """ +OBORPC Decorator """ import inspect +from typing import Callable -def procedure(fun): +def procedure(fun: Callable): + """ + Marks the method with this decorator to make it available + for RPC within the class with direct inheritance with `OBORBase`. + To make it works also make sure the method is overridden + in the server class inheritance of the base class + + from oborpc.base import meta + from oborpc.decorator import procedure + + class Calculator(meta.OborBase): + @procedure + def add(self, a, b): + pass + + class CalculatorServer(Calculator): + def tambah(self, a, b): + return a + b + """ if not inspect.isfunction(fun): raise TypeError("can only applied for function or method") fun.__isoborprocedure__ = True diff --git a/oborpc/exception.py b/oborpc/exception.py new file mode 100644 index 0000000..b97547d --- /dev/null +++ b/oborpc/exception.py @@ -0,0 +1,17 @@ +""" +OBORPC Exceptions +""" + +class OBORPCBuildException(Exception): + """ + Build Related Exceptions + """ + def __init__(self, *args: object) -> None: + super().__init__(*args) + +class RPCCallException(Exception): + """ + Any Exception during RPC Call + """ + def __init__(self, *args: object) -> None: + super().__init__(*args) diff --git a/setup.py b/setup.py index 5a91488..7896b92 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,10 @@ +""" +Setup script +""" +from pathlib import Path from setuptools import setup, find_packages # read the contents of your README file -from pathlib import Path this_directory = Path(__file__).parent long_description = (this_directory / "README.md").read_text()