Source code for smartrpyc.server.base

"""
Base objects for the RPC
"""

from collections import defaultdict
import logging

import zmq

from smartrpyc.utils import lazy_property
from smartrpyc.utils.serialization import MsgPackSerializer
from .register import MethodsRegister
from .exceptions import DirectResponse, SetMethod

__all__ = ['Server', 'Request']

logger = logging.getLogger(__name__)


[docs]class Request(object): """Wrapper for requests from the RPC""" server = None def __init__(self, raw): """ :param raw: The (unpacked) content of the request """ self.raw = raw @property
[docs] def id(self): """Id of the request""" return self.raw['i']
@property
[docs] def route(self): """Route of the request""" return self.raw.get('r', '')
@property
[docs] def method(self): """Name of the method to be called""" return self.raw['m']
@property
[docs] def args(self): """Positional arguments for the called method""" return self.raw.get('a') or ()
@property
[docs] def kwargs(self): """Keyword arguments for the called method""" return self.raw.get('k') or {}
[docs]class Server(object): request_class = Request packer = MsgPackSerializer middleware = None def __init__(self, methods=None): """ Constructor for the RPC Server class :param methods: Methods to be exposed via RPC. Usually a ``MethodsRegister`` instance, but this is not mandatory. It has to have a ``lookup(name)`` method, returning the specified method, or raising an exception if the method was not found. """ self._routes = defaultdict(MethodsRegister) if methods is not None: self.methods = methods self.middleware = [] # Middleware chain @property def methods(self): return self.routes[''] @methods.setter
[docs] def methods(self, value): assert isinstance(value, MethodsRegister) self.routes[''] = value
@property
[docs] def routes(self): ## Prevent direct assignment ## DAFUQ?? # if not all(isinstance(v, MethodsRegister) # for k, v in self._routes.iteritems()): # import pdb # pdb.set_trace() return self._routes
@lazy_property
[docs] def socket(self): context = zmq.Context() return context.socket(zmq.REP)
[docs] def bind(self, addresses): """ Bind the server socket to an address (or a list of) **Beware!** Only ``.bind()`` or ``.connect()`` may be called on a given server instance! :params addresses: A (list of) address(es) to which to bind the server. """ if not isinstance(addresses, (list, tuple)): addresses = [addresses] for addr in addresses: self.socket.bind(addr)
[docs] def connect(self, addresses): """ Connect the server socket to an address (or a list of) **Beware!** Only ``.bind()`` or ``.connect()`` may be called on a given server instance! :params addresses: A (list of) address(es) to which to connect the server. """ if not isinstance(addresses, (list, tuple)): addresses = [addresses] for addr in addresses: self.socket.connect(addr)
[docs] def run(self): """Start the server listening loop""" while True: self.run_once()
[docs] def run_once(self): """Run once: process a request and send a response""" message_raw = self.socket.recv() try: request = self.request_class(self.packer.unpackb(message_raw)) request.server = self response = self._process_request(request) except Exception, e: ## If anything bad happened, send an exception message ## to the user. ## We need to make damn sure something is sent to the client, ## or the request will hang forever.. ## We might as well want a way to timeout connections, to prevent ## hanging at all... self.socket.send( self.packer.packb( self._exception_message(e))) else: ## Send the response message to the client self.socket.send(self.packer.packb(response))
def _lookup_method(self, request): """Find method to be used for this request""" if request.route not in self.routes: raise KeyError("No such route: {0!r}".format(request.route)) route = self.routes[request.route] try: return route.lookup(request.method) except: raise KeyError("No such method: {method!r} (route: {route!r})" "".format(route=request.route, method=request.method)) def _process_request(self, request): """Process a received request""" logger.debug("Processing request") exception = None method = None ## Lookup the requested method try: method = self._lookup_method(request) except KeyError, e: logger.error(str(e)) ## We don't terminate here, as a "pre" middleware ## might have a solution for this.. exception = e ## Execute all the PRE middleware on the request try: self._exec_pre_middleware(request, method) except DirectResponse, e: logger.debug("A PRE middleware requested to send a response now") return self._response_message(e.response) except SetMethod, e: logger.debug("A PRE middleware changed the method to be called") method = e.method exception = None # Clear exceptions happened before.. except Exception, e: logger.exception('Exception during execution of "pre" middleware') return self._exception_message(e) ## If the method is still None, return the current exception if method is None: if exception is None: exception = KeyError("No such method") return self._exception_message(exception) ## Execute the method try: response = method(request, *request.args, **request.kwargs) except Exception, e: logger.exception('Exception during execution of request') response = None exception = e else: exception = None ## Execute all the POST middleware on request + response try: response = self._exec_post_middleware( request, method, response, exception) except DirectResponse, e: logger.info( 'A "post" middleware requested immediate returning ' 'of a response') return self._response_message(e.response) except Exception, e: logger.exception('Exception during execution of "post" middleware') ## Exceptions in the POST middleware are fatal.. return self._exception_message(e) ## If there was an exception, return it as a special message if exception is not None: return self._exception_message(exception) ## Return the response message return self._response_message(response) def _exception_message(self, exception): """ Create a response object indicating an exception occurred. :param exception: The original exception :return: An object with the ``e`` (name) and ``e_msg`` (message) keys. """ return { 'e': type(exception).__name__, 'e_msg': str(exception), } def _response_message(self, response): """ Create a "normal" response message. The message is simply a dict with an ``r`` key containing the result value. This is needed to distinguish between return values and exceptions. """ return {'r': response} def _exec_pre_middleware(self, request, method): """ Run all the "pre" middleware functions :param request: The request being processed :param method: The method that will be used to process the request """ for mw in self.middleware: if hasattr(mw, 'pre'): mw.pre(request, method) def _exec_post_middleware(self, request, method, response, exception): """ Run all the "post" middleware functions :param request: The original request object :param method: The figured out method for the request :param response: Response from the method (if any) :param exception: Exception raised from the method (if any) """ for mw in reversed(self.middleware): if hasattr(mw, 'post'): retval = mw.post(request, method, response, exception) if retval is not None: response = retval return response