from abc import abstractmethod
from collections import namedtuple
from warnings import warn
from pyjackson import deserialize, serialize
import ebonite
from ebonite.runtime.interface.base import InterfaceDescriptor, InterfaceMethodDescriptor
from ebonite.utils.log import logger
[docs]class BaseClient:
"""
Base class for clients of Ebonite runtime.
User method calls are transparently proxied to :class:`~ebonite.runtime.interface.base.Interface` deployed on
:class:`~ebonite.runtime.server.base.Server`.
PyJackson is always used for serialization of inputs and deserialization of outputs.
"""
def __init__(self):
self.methods = {}
interface: InterfaceDescriptor = self._interface_factory()
if ebonite.__version__ != interface.version:
warn(f"Server Ebonite version {interface.version}, client Ebonite version {ebonite.__version__}")
for method in interface.methods:
self.methods[method.name] = _bootstrap_method(method)
@abstractmethod
def _interface_factory(self) -> InterfaceDescriptor:
"""
Takes interface deployed on server to validate method calls at client side and
correctly (de)serialize inputs/outputs via PyJackson
:return: :class:`.InterfaceDescriptor` describing supported methods
"""
pass # pragma: no cover
@abstractmethod
def _call_method(self, name, args):
"""
Performs method call at server side
:param name: name of method to call
:param args: `dict` of (name, value) mappings for arguments. Values are PyJackson-serialized objects.
:return: method return value which should be PyJackson deserializable.
"""
pass # pragma: no cover
def __getattr__(self, name):
if name not in self.methods:
raise KeyError(f'{name} method is not exposed by server')
return _MethodCall(self.base_url, self.methods[name], self._call_method)
_Argument = namedtuple('Argument', ('name', 'type'))
_Method = namedtuple('Method', ('name', 'args', 'out_type'))
class _MethodCall:
def __init__(self, base_url, method: _Method, call_method):
self.base_url = base_url
self.method = method
self.call_method = call_method
def __call__(self, *args, **kwargs):
if args and kwargs:
raise ValueError('Parameters should be passed either in positional or in keyword fashion, not both')
if len(args) > len(self.method.args) or len(kwargs) > len(self.method.args):
raise ValueError(f'Too much parameters given, expected: {len(self.method.args)}')
data = {}
for i, arg in enumerate(self.method.args):
obj = None
if len(args) > i:
obj = args[i]
if arg.name in kwargs:
obj = kwargs[arg.name]
if obj is None:
raise ValueError(f'Parameter with name "{arg.name}" (position {i}) should be passed')
data[arg.name] = serialize(obj, arg.type)
logger.debug('Calling server method "%s", args: %s ...', self.method.name, data)
out = self.call_method(self.method.name, data)
logger.debug('Server call returned %s', out)
return deserialize(out, self.method.out_type)
def _bootstrap_method(method: InterfaceMethodDescriptor):
logger.debug('Bootstraping server method "%s" with %s argument(s)...', method.name, len(method.args))
args = []
for arg_name, arg_type in method.args.items():
args.append(_Argument(arg_name, arg_type))
return _Method(method.name, args, method.out_type)