Skip to content

Commit

Permalink
Merge pull request #1 from Danangjoyoo/feature/initial-module
Browse files Browse the repository at this point in the history
[FEAT] initial module creation
  • Loading branch information
Danangjoyoo committed Jan 12, 2024
2 parents b5acd26 + 86c8a71 commit fdc7e34
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 0 deletions.
Empty file added oborpc/__init__.py
Empty file.
Empty file added oborpc/base/__init__.py
Empty file.
25 changes: 25 additions & 0 deletions oborpc/base/meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Meta File
"""

class OBORMeta(type):
"""
Meta class used
"""
__obor_registry__ = {}
def __new__(mcls, name, bases, namespace, /, **kwargs):
cls = super().__new__(mcls, name, bases, namespace, **kwargs)

cls.__oborprocedures__ = {
methodname for methodname, value in namespace.items()
if getattr(value, "__isoborprocedure__", False)
}
OBORMeta.__obor_registry__[cls] = cls.__oborprocedures__

return cls


class OBORBase(metaclass=OBORMeta):
"""
Obor Base Class
"""
5 changes: 5 additions & 0 deletions oborpc/builder/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from ._base import OBORBuilder
from ._client import ClientBuilder
from ._server import ServerBuilder
from ._fastapi import FastAPIServerBuilder
from ._flask import FlaskServerBuilder
32 changes: 32 additions & 0 deletions oborpc/builder/_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
"""

class OBORBuilder():
__registered_base = set()

def __init__(self, host, port=None, timeout=1, retry=0) -> None:
self.master_instances = []
self.host = host
self.port = port
self.timeout = timeout
self.retry = retry

protocol = "http://"
if self.check_has_protocol(host):
protocol = ""

self.base_url = f"{protocol}{host}"
if port:
self.base_url += f":{port}"

def check_has_protocol(self, host: str):
if host.startswith("http://"):
return True
if host.startswith("https://"):
return True
return False

def check_registered_base(self, base: str):
if base in OBORBuilder.__registered_base:
raise Exception(f"Failed to build client RPC {base} : base class can only built once")
OBORBuilder.__registered_base.add(base)
51 changes: 51 additions & 0 deletions oborpc/builder/_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
"""
import inspect
import json
import requests
import time
from ._base import OBORBuilder


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):
def remote_call(*args, **kwargs):
try:
t0 = time.time()
data = {
"args": args[1:],
"kwargs": kwargs
}
url = f"{self.base_url}{url_prefix}/{class_name}/{method_name}"
response = requests.post(
url,
headers={"Content-Type": "application/json"},
json=json.dumps(data),
timeout=timeout if timeout != None else self.timeout
)
if not response:
raise Exception(f"rpc call failed method={method_name}")
return response.json().get("data")
except Exception as e:
_retry = retry if retry != None else self.retry
if _retry:
return remote_call(*args, **kwargs, retry=_retry-1)
raise Exception(f"rpc call failed method={method_name}")
finally:
# print("elapsed", time.time() - t0)
pass
return remote_call

def setup_client_rpc(self, instance: object, url_prefix: str = ""):
_class = instance.__class__
iterator_class = _class

self.check_registered_base(_class)

for (name, method) 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))
77 changes: 77 additions & 0 deletions oborpc/builder/_fastapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
"""
import json
import asyncio
from enum import Enum
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)

def create_remote_responder(
self,
instance: OBORBase,
router: APIRouter,
class_name: str,
method_name: str,
method: Callable
):
@router.post(f"{router.prefix}/{class_name}/{method_name}")
def final_func(request: Request):
request_body = asyncio.run(request.body())
body = json.loads(json.loads(request_body.decode()))
return self.dispatch_rpc_request(instance, method, body)

def build_router_from_instance(
self,
instance: OBORBase,
*,
prefix: str,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
default_response_class: Type[Response] = Default(JSONResponse),
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
callbacks: Optional[List[BaseRoute]] = None,
routes: Optional[List[BaseRoute]] = None,
redirect_slashes: bool = True,
default: Optional[ASGIApp] = None,
dependency_overrides_provider: Optional[Any] = None,
route_class: Type[APIRoute] = APIRoute,
on_startup: Optional[Sequence[Callable[[], Any]]] = None,
on_shutdown: Optional[Sequence[Callable[[], Any]]] = None,
lifespan: Optional[Lifespan[Any]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
generate_unique_id_function: Callable[[APIRoute], str] = Default(generate_unique_id),
):
"""
"""
router = APIRouter(
prefix=prefix,
tags=tags,
dependencies=dependencies,
default_response_class=default_response_class,
responses=responses,
callbacks=callbacks,
routes=routes,
redirect_slashes=redirect_slashes,
default=default,
dependency_overrides_provider=dependency_overrides_provider,
route_class=route_class,
on_startup=on_startup,
on_shutdown=on_shutdown,
lifespan=lifespan,
deprecated=deprecated,
include_in_schema=include_in_schema,
generate_unique_id_function=generate_unique_id_function
)

self.setup_server_rpc(instance, router)

return router
60 changes: 60 additions & 0 deletions oborpc/builder/_flask.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
"""
import functools
import json
import os
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)

def create_remote_responder(
self, instance: OBORBase, router: Blueprint, class_name, method_name, method
):
def create_modified_func():
@functools.wraps(method)
def modified_func():
body = json.loads(flask_request.get_json())
return self.dispatch_rpc_request(
instance, method, body
)
return modified_func
router.post(
f"{router.url_prefix or ''}/{class_name}/{method_name}"
)(create_modified_func())

def build_blueprint_from_instance(
self,
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()
):
"""
"""
blueprint = Blueprint(
blueprint_name,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_group
)

self.setup_server_rpc(instance, blueprint)

return blueprint
32 changes: 32 additions & 0 deletions oborpc/builder/_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
"""
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):
raise NotImplementedError("method should be overridden")

def dispatch_rpc_request(self, instance, method, body):
args = body.get("args", [])
kwargs = body.get("kwargs", {})
res = method(instance, *args, **kwargs)
return {"data": res}

def setup_server_rpc(self, instance: object, router):
_class = instance.__class__
iterator_class = instance.__class__.__base__
method_map = {
name: method for (name, method) in inspect.getmembers(
_class, predicate=inspect.isfunction
)
}

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))
9 changes: 9 additions & 0 deletions oborpc/decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
"""
import inspect

def procedure(fun):
if not inspect.isfunction(fun):
raise TypeError("can only applied for function or method")
fun.__isoborprocedure__ = True
return fun
34 changes: 34 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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()

VERSION = "0.0.1"
DESCRIPTION = "An easy setup object oriented RPC. Built-in setup for FastAPI and Flask"

# Setting up
setup(
name="oborpc",
version=VERSION,
author="danangjoyoo (Agus Danangjoyo)",
author_email="<agus.danangjoyo.blog@gmail.com>",
description=DESCRIPTION,
long_description_content_type="text/markdown",
long_description=long_description,
packages=find_packages(),
install_requires=[],
keywords=["fastapi", "flask", "rpc", "OOP"],
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Environment :: Web Environment",
"Operating System :: OS Independent",
"Typing :: Typed"
],
url="https://github.com/Danangjoyoo/oborpc"
)

0 comments on commit fdc7e34

Please sign in to comment.