bjsonrpc-0.2.0/0000755000175000017500000000000011554316615012612 5ustar deaviddeavidbjsonrpc-0.2.0/bjsonrpc/0000755000175000017500000000000011554316615014432 5ustar deaviddeavidbjsonrpc-0.2.0/bjsonrpc/__init__.py0000644000175000017500000000120511554312752016537 0ustar deaviddeavid""" Copyright (c) 2010 David Martinez Marti All rights reserved. Licensed under 3-clause BSD License. See LICENSE.txt for the full license text. """ __version__ = '0.2' __release__ = '0.2.0' __all__ = [ "createserver", "connect", "server", "connection", "request", "handlers", "proxies", "jsonlib", "exceptions" ] bjsonrpc_options = { 'threaded' : False } from bjsonrpc.main import createserver, connect import bjsonrpc.server import bjsonrpc.connection import bjsonrpc.request import bjsonrpc.handlers import bjsonrpc.proxies import bjsonrpc.jsonlib import bjsonrpc.exceptions bjsonrpc-0.2.0/bjsonrpc/connection.py0000644000175000017500000006614411554311744017154 0ustar deaviddeavid""" bjson/connection.py Asynchronous Bidirectional JSON-RPC protocol implementation over TCP/IP Copyright (c) 2010 David Martinez Marti All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ import socket, traceback, sys, threading from types import MethodType, FunctionType from bjsonrpc.proxies import Proxy from bjsonrpc.request import Request from bjsonrpc.exceptions import EofError, ServerError from bjsonrpc import bjsonrpc_options import bjsonrpc.jsonlib as json import select class RemoteObject(object): """ Represents a object in the server-side (or client-side when speaking from the point of view of the server) . It remembers its name in the server-side to allow calls to the original object. Parameters: **conn** Connection object which holds the socket to the other end of the communications **obj** JSON object (Python dictionary) holding the values recieved. It is used to retrieve the properties to create the remote object. (Initially only used to get object name) Example:: list = conn.call.newList() for i in range(10): list.notify.add(i) print list.call.getitems() """ name = None # Name of the object in the server-side. call = None # Synchronous Proxy. It forwards your calls to it to the other end, waits # the response and returns the value. method = None # Asynchronous Proxy. It forwards your calls to it to the other end and # inmediatelly returns a *request.Request* instance. notify = None # Notification Proxy. It forwards your calls to it to the other end and # tells the server to not response even if there's any error in the call. # Returns *None*. @property def connection(self): """ Public property to get the internal connection object. """ return self._conn def __init__(self, conn, obj): self._conn = conn self.name = obj['__remoteobject__'] self.call = Proxy(self._conn, obj=self.name, sync_type=0) self.method = Proxy(self._conn, obj=self.name, sync_type=1) self.notify = Proxy(self._conn, obj=self.name, sync_type=2) def __del__(self): self._close() def _close(self): """ Internal close method called both by __del__() and public method close() """ self.call.__delete__() self.name = None def close(self): """ Closes/deletes the remote object. The server may or may not delete it at this time, but after this call we don't longer have any access to it. This method is automatically called when Python deletes this instance. """ return self._close() class Connection(object): # TODO: Split this class in simple ones """ Represents a communiation tunnel between two parties. **sck** Connected socket to use. Should be an instance of *socket.socket* or something compatible. **address** Address of the other peer in (host,port) form. It is only used to inform handlers about the peer address. **handler_factory** Class type inherited from BaseHandler which holds the public methods. It defaults to *NullHandler* meaning no public methods will be avaliable to the other end. """ _maxtimeout = { 'read' : 60, # default maximum read timeout. 'write' : 60, # default maximum write timeout. } @classmethod def setmaxtimeout(cls, operation, value): """ Set the maximum timeout in seconds for **operation** operation. Parameters: **operation** The operation which has to be configured. Can be either 'read' or 'write'. **value** The timeout in seconds as a floating number. If is None, will block until succeed. If is 0, will be nonblocking. """ assert(operation in ['read', 'write']) cls._maxtimeout[operation] = value @classmethod def getmaxtimeout(cls, operation): """ Get the maximum timeout in seconds for **operation** operation. Parameters: **operation** The operation which has to be configured. Can be either 'read' or 'write'. **(return value)** The timeout in seconds as a floating number or None. """ if operation not in cls._maxtimeout: return None return cls._maxtimeout[operation] call = None # Synchronous Proxy. It forwards your calls to it to the other end, waits # the response and returns the value. method = None # Asynchronous Proxy. It forwards your calls to it to the other end and # inmediatelly returns a *request.Request* instance. notify = None # Notification Proxy. It forwards your calls to it to the other end and # tells the server to not response even if there's any error in the call. # Returns *None*. def __init__(self, sck, address = None, handler_factory = None): self._debug_socket = False self._debug_dispatch = False self._buffer = '' self._sck = sck self._address = address self._handler = handler_factory if self._handler: self.handler = self._handler(self) self._id = 0 self._requests = {} self._objects = {} self.scklock = threading.Lock() self.call = Proxy(self, sync_type=0) self.method = Proxy(self, sync_type=1) self.notify = Proxy(self, sync_type=2) self._wbuffer = [] self.write_lock = threading.RLock() self.read_lock = threading.RLock() self.reading_event = threading.Event() self.threaded = bjsonrpc_options['threaded'] @property def socket(self): """ public property that holds the internal socket used. """ return self._sck def get_id(self): """ Retrieves a new ID counter. Each connection has a exclusive ID counter. It is mainly used to create internal id's for calls. """ self._id += 1 return self._id def load_object(self, obj): """ Helper function for JSON loads. Given a dictionary (javascript object) returns an apropiate object (a specific class) in certain cases. It is mainly used to convert JSON hinted classes back to real classes. Parameters: **obj** Dictionary-like object to test. **(return value)** Either the same dictionary, or a class representing that object. """ if '__remoteobject__' in obj: return RemoteObject(self, obj) if '__objectreference__' in obj: return self._objects[obj['__objectreference__']] if '__functionreference__' in obj: name = obj['__functionreference__'] if '.' in name: objname, methodname = name.split('.') obj = self._objects[objname] else: obj = self.handler methodname = name method = obj.get_method(methodname) return method return obj def addrequest(self, request): """ Adds a request to the queue of requests waiting for response. """ assert(isinstance(request, Request)) assert(request.request_id not in self._requests) self._requests[request.request_id] = request def dump_object(self, obj): """ Helper function to convert classes and functions to JSON objects. Given a incompatible object called *obj*, dump_object returns a JSON hinted object that represents the original parameter. Parameters: **obj** Object, class, function,etc which is incompatible with JSON serialization. **(return value)** A valid serialization for that object using JSON class hinting. """ # object of unknown type if type(obj) is FunctionType or type(obj) is MethodType : conn = getattr(obj, '_conn', None) if conn != self: raise TypeError("Tried to serialize as JSON a handler for " "another connection!") return self._dump_functionreference(obj) if not isinstance(obj, object): raise TypeError("JSON objects must be new-style classes") if not hasattr(obj, '__class__'): raise TypeError("JSON objects must be instances, not types") if obj.__class__.__name__ == 'Decimal': # Probably is just a float. return float(obj) if isinstance(obj, RemoteObject): return self._dump_objectreference(obj) if hasattr(obj, 'get_method'): return self._dump_remoteobject(obj) raise TypeError("Python object %s laks a 'get_method' and " "is not serializable!" % repr(obj)) def _dump_functionreference(self, obj): """ Converts obj to a JSON hinted-class functionreference""" return { '__functionreference__' : obj.__name__ } def _dump_objectreference(self, obj): """ Converts obj to a JSON hinted-class objectreference""" return { '__objectreference__' : obj.name } def _dump_remoteobject(self, obj): """ Converts obj to a JSON hinted-class remoteobject, creating a RemoteObject if necessary """ # An object can be remotely called if : # - it derives from object (new-style classes) # - it is an instance # - has an internal function _get_method to handle remote calls if not hasattr(obj, '__remoteobjects__'): obj.__remoteobjects__ = {} if self in obj.__remoteobjects__: instancename = obj.__remoteobjects__[self] else: classname = obj.__class__.__name__ instancename = "%s_%04x" % (classname.lower(), self.get_id()) self._objects[instancename] = obj obj.__remoteobjects__[self] = instancename return { '__remoteobject__' : instancename } def _dispatch_method(self, request): """ Processes one request. """ # TODO: Simplify this function or split it in small ones. req_id = request.get("id", None) req_method = request.get("method") req_args = request.get("params", []) if type(req_args) is dict: req_kwargs = req_args req_args = [] else: req_kwargs = request.get("kwparams", {}) if req_kwargs: req_kwargs = dict((str(k), v) for k, v in req_kwargs.iteritems()) if '.' in req_method: # local-object. objectname, req_method = req_method.split('.')[:2] if objectname not in self._objects: raise ValueError, "Invalid object identifier" if req_method == '__delete__': req_object = None try: self._objects[objectname]._shutdown() except Exception: print "Error when shutting down the object", type(self._objects[objectname]),":" print traceback.format_exc() del self._objects[objectname] result = None else: req_object = self._objects[objectname] else: req_object = self.handler if req_object: try: req_function = req_object.get_method(req_method) result = req_function(*req_args, **req_kwargs) except ServerError, exc: if req_id is not None: return {'result': None, 'error': '%s' % (exc), 'id': req_id} except Exception: etype, evalue, etb = sys.exc_info() funargs = ", ".join( [repr(x) for x in req_args] + ["%s=%s" % (k, repr(x)) for k, x in req_kwargs.iteritems()] ) if len(funargs) > 40: funargs = funargs[:37] + "..." print "(%s) In Handler method %s.%s(%s) " % ( req_object.__class__.__module__, req_object.__class__.__name__, req_method, funargs ) print "\n".join([ "%s::%s:%d %s" % ( filename, fnname, lineno, srcline ) for filename, lineno, fnname, srcline in traceback.extract_tb(etb)[1:] ]) print "Unhandled error: %s: %s" % (etype.__name__, evalue) del etb if req_id is not None: return { 'result': None, 'error': '%s: %s' % (etype.__name__, evalue), 'id': req_id } if req_id is None: return None return {'result': result, 'error': None, 'id': req_id} def dispatch_until_empty(self): """ Calls *read_and_dispatch* method until there are no more messages to dispatch in the buffer. Returns the number of operations that succeded. This method will never block waiting. If there aren't any more messages that can be processed, it returns. """ ready_to_read = select.select( [self._sck], # read [], [], # write, errors 0 # timeout )[0] if not ready_to_read: return 0 newline_idx = 0 count = 0 while newline_idx != -1: if not self.read_and_dispatch(timeout=0): break count += 1 newline_idx = self._buffer.find('\n') return count def read_and_dispatch(self, timeout=None, thread=True, condition=None): """ Read one message from socket (with timeout specified by the optional argument *timeout*) and dispatches that message. Parameters: **timeout** = None Timeout in seconds of the read operation. If it is None (or ommitted) then the read will wait until new data is available. **(return value)** True, in case of the operation has suceeded and **one** message has been dispatched. False, if no data or malformed data has been received. """ self.read_lock.acquire() self.reading_event.set() try: if condition: if condition() == False: return False if thread: dispatch_item = self.dispatch_item_threaded else: dispatch_item = self.dispatch_item_single data = self.read(timeout=timeout) if not data: return False try: item = json.loads(data, self) if type(item) is list: # batch call for i in item: dispatch_item(i) elif type(item) is dict: # std call if 'result' in item: self.dispatch_item_single(item) else: dispatch_item(item) else: # Unknown format :-( print "Received message with unknown format type:" , type(item) return False except Exception: print traceback.format_exc() return False return True finally: self.reading_event.clear() self.read_lock.release() def dispatch_item_threaded(self, item): if self.threaded: th1 = threading.Thread(target = self.dispatch_item_single, args = [ item ] ) th1.start() return True else: return self.dispatch_item_single(item) def dispatch_item_single(self, item): """ Given a JSON item received from socket, determine its type and process the message. """ assert(type(item) is dict) response = None if 'id' not in item: item['id'] = None if 'method' in item: response = self._dispatch_method(item) elif 'result' in item: assert(item['id'] in self._requests) request = self._requests[item['id']] del self._requests[item['id']] request.setresponse(item) else: response = { 'result': None, 'error': "Unknown format", 'id': item['id'] } if response is not None: txtResponse = None try: txtResponse = json.dumps(response, self) except Exception, e: print "An unexpected error ocurred when trying to create the message:", repr(e) response = { 'result': None, 'error': "InternalServerError: " + repr(e), 'id': item['id'] } txtResponse = json.dumps(response, self) try: self.write(txtResponse) except TypeError: print "response was:", repr(response) raise return True def proxy(self, sync_type, name, args, kwargs): """ Call method on server. sync_type :: = 0 .. call method, wait, get response. = 1 .. call method, inmediate return of object. = 2 .. call notification and exit. """ data = {} data['method'] = name if sync_type in [0, 1]: data['id'] = self.get_id() if len(args) > 0: data['params'] = args if len(kwargs) > 0: if len(args) == 0: data['params'] = kwargs else: data['kwparams'] = kwargs if sync_type == 2: # short-circuit for speed! self.write(json.dumps(data, self)) return None req = Request(self, data) if sync_type == 2: return None if sync_type == 1: return req return req.value def close(self): """ Close the connection and the socket. """ try: self.handler._shutdown() except Exception: print "Error when shutting down the handler:" print traceback.format_exc() try: self._sck.shutdown(socket.SHUT_RDWR) except socket.error: pass self._sck.close() def write_line(self, data): """ Write a line *data* to socket. It appends a `\\n` at the end of the *data* before sending it. The string MUST NOT contain `\\n` otherwise an AssertionError will raise. Parameters: **data** String containing the data to be sent. """ assert('\n' not in data) self.write_lock.acquire() try: if self._debug_socket: print "<:%d:" % len(data), data[:130] self._wbuffer += list(str(data + '\n')) sbytes = 0 while len(self._wbuffer) > 0: try: sbytes = self._sck.send("".join(self._wbuffer)) except IOError: print "Read socket error: IOError (timeout: %s)" % ( repr(self._sck.gettimeout()) ) print traceback.format_exc(0) return '' except socket.error: print "Read socket error: socket.error (timeout: %s)" % ( repr(self._sck.gettimeout()) ) print traceback.format_exc(0) return '' except: raise if sbytes == 0: break self._wbuffer[0:sbytes] = [] if len(self._wbuffer): print "warn: %d bytes left in write buffer" % len(self._wbuffer) return len(self._wbuffer) finally: self.write_lock.release() def read_line(self): """ Read a line of *data* from socket. It removes the `\\n` at the end before returning the value. If the original packet contained `\\n`, the message will be decoded as two or more messages. Returns the line of *data* received from the socket. """ self.read_lock.acquire() try: data = self._readn() if len(data) and self._debug_socket: print ">:%d:" % len(data), data[:130] return data finally: self.read_lock.release() def settimeout(self, operation, timeout): """ configures a timeout for the connection for a given operation. operation is one of "read" or "write" """ if operation in self._maxtimeout: maxtimeout = self._maxtimeout[operation] else: maxtimeout = None if maxtimeout is not None: if timeout is None or timeout > maxtimeout: timeout = maxtimeout self._sck.settimeout(timeout) def write(self, data, timeout = None): """ Standard function to write to the socket which by default points to write_line """ self.settimeout("write", timeout) self.scklock.acquire() ret = None try: ret = self.write_line(data) finally: self.scklock.release() return ret def read(self, timeout = None): """ Standard function to read from the socket which by default points to read_line """ self.settimeout("read", timeout) ret = None self.scklock.acquire() try: ret = self.read_line() finally: self.scklock.release() return ret def _readn(self): """ Internal function which reads from socket waiting for a newline """ streambuffer = self._buffer pos = streambuffer.find('\n') #print "read..." #retry = 0 while pos == -1: data = '' try: data = self._sck.recv(2048) except IOError, inst: print "Read socket error: IOError (timeout: %s)" % ( repr(self._sck.gettimeout()) ) print inst.args val = inst.args[0] if val == 11: # Res. Temp. not available. if self._sck.gettimeout() == 0: # if it was too fast self._sck.settimeout(5) continue #time.sleep(0.5) #retry += 1 #if retry < 10: # print "Retry ", retry # continue #print traceback.format_exc(0) return '' except socket.error, inst: print "Read socket error: socket.error (timeout: %s)" % ( repr(self._sck.gettimeout()) ) print inst.args #print traceback.format_exc(0) return '' except: raise if not data: raise EofError(len(streambuffer)) #print "readbuf+:",repr(data) streambuffer += data pos = streambuffer.find('\n') self._buffer = streambuffer[pos + 1:] streambuffer = streambuffer[:pos] #print "read:", repr(buffer) return streambuffer def serve(self): """ Basic function to put the connection serving. Usually is better to use server.Server class to do this, but this would be useful too if it is run from a separate Thread. """ try: while True: self.read_and_dispatch() finally: self.close() bjsonrpc-0.2.0/bjsonrpc/exceptions.py0000644000175000017500000000374011511304531017154 0ustar deaviddeavid""" bjson/exceptions.py Asynchronous Bidirectional JSON-RPC protocol implementation over TCP/IP Copyright (c) 2010 David Martinez Marti All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ class ServerError(Exception): """ Exception raised whenever an error occurs in the other end handling your request. """ pass class EofError(Exception): """ End-of-file error raised whenever the socket reaches the end of the sream. """ pass bjsonrpc-0.2.0/bjsonrpc/handlers.py0000644000175000017500000001251111554251415016600 0ustar deaviddeavid""" bjson/handlers.py Asynchronous Bidirectional JSON-RPC protocol implementation over TCP/IP Copyright (c) 2010 David Martinez Marti All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ import re from types import MethodType from bjsonrpc.exceptions import ServerError class BaseHandler(object): """ Base Class to publish remote methods. It is instantiated by *Connection*. Example:: class MyHandler(bjson.handlers.BaseHandler): def _setup(self): # Initailize here. self.c = 0 def echo(self,text): print text self.c += 1 def getcount(self): return c """ public_methods_pattern = r'^[a-z]\w+$' # Pattern to know which methods should be automatically published nonpublic_methods = [ "close", "add_method", "get_method", ] # List of method names that never should be published def __init__(self, connection): self._conn = connection if hasattr(self._conn,"connection"): self._conn = self._conn.connection if hasattr(self._conn,"_conn"): self._conn = self._conn._conn self._methods = {} for mname in dir(self): if re.match(self.public_methods_pattern, mname): function = getattr(self, mname) if isinstance(function, MethodType): self.add_method(function) self._setup() def _setup(self): """ Empty method to ease inheritance. Overload it with your needs, it will be called after __init__. """ pass def _shutdown(self): """ Internal method called when the handler is going to be destroyed. You should add cleanup code here. Remember to call the parent function. """ pass # In the future, here we could have some internal clean-up code. def close(self): """ Cleans some variables before the object is freed. _close is called manually from connection whenever a handler is going to be deleted. """ self._methods = {} def add_method(self, *args, **kwargs): """ Porcelain for publishing methods into the handler. Is used by the constructor itself to publish all non-private functions. """ for method in args: if method.__name__ in self.nonpublic_methods: continue try: assert(method.__name__ not in self._methods) except AssertionError: raise NameError, "Method with name %s already in the class methods!" % (method.__name__) self._methods[method.__name__] = method for name, method in kwargs.iteritems(): if method.__name__ in self.nonpublic_methods: continue try: assert(name not in self._methods) except AssertionError: raise NameError, "Method with name %s already in the class methods!" % (method.__name__) self._methods[name] = method def get_method(self, name): """ Porcelain for resolving method objects from their names. Used by connections to get the apropiate method object. """ if name not in self._methods: raise ServerError("Unknown method %s" % repr(name)) return self._methods[name] class NullHandler(BaseHandler): """ Null version of BaseHandler which has nothing in it. Use this when you don't want to publish any function. """ pass bjsonrpc-0.2.0/bjsonrpc/jsonlib.py0000644000175000017500000000522311511304531016431 0ustar deaviddeavid""" bjson/jsonlib.py Asynchronous Bidirectional JSON-RPC protocol implementation over TCP/IP Copyright (c) 2010 David Martinez Marti All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ try: import simplejson as j except ImportError: import json as j except ImportError: print "FATAL: No suitable json library found!" raise from pprint import pprint def dumps(argobj, conn): """ dumps json object using loaded json library and forwards unknown objects to *Connection.dumpObject* function. """ ret = None try: ret = j.dumps(argobj, separators = (',', ':'), default=conn.dump_object) except TypeError: pprint(argobj) raise #raise TypeError("The Python object is not serializable to JSON!") return ret def loads(argobj, conn): """ loads json object using *Connection.load_object* to convert json hinted objects to real objects. """ ret = None try: ret = j.loads(argobj, object_hook=conn.load_object) except ValueError: pprint(argobj) raise #raise ValueError("The String object is not a valid JSON data!") return retbjsonrpc-0.2.0/bjsonrpc/main.py0000644000175000017500000000530411511304531015715 0ustar deaviddeavid""" bjson/main.py Copyright (c) 2010 David Martinez Marti All rights reserved. Licensed under 3-clause BSD License. See LICENSE.txt for the full license text. """ import socket import bjsonrpc.server import bjsonrpc.connection import bjsonrpc.handlers __all__ = [ "createserver", "connect", ] def createserver(host="127.0.0.1", port=10123, handler_factory=bjsonrpc.handlers.NullHandler): """ Creates a *bjson.server.Server* object linked to a listening socket. Parameters: **host** Address (IP or Host Name) to listen to as in *socket.bind*. Use "0.0.0.0" to listen to all address. By default this points to 127.0.0.1 to avoid security flaws. **port** Port number to bind the socket. In Unix, port numbers less than 1024 requires special permissions. **handler_factory** Class to instantiate to publish remote functions. **(return value)** A *bjson.server.Server* instance or raises an exception. Servers are usually created this way:: import bjsonrpc server = bjsonrpc.createserver("0.0.0.0") server.serve() Check :ref:`bjsonrpc.server` documentation """ sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sck.bind((host, port)) sck.listen(3) return bjsonrpc.server.Server(sck, handler_factory=handler_factory) def connect(host="127.0.0.1", port=10123, handler_factory=bjsonrpc.handlers.NullHandler): """ Creates a *bjson.connection.Connection* object linked to a connected socket. Parameters: **host** Address (IP or Host Name) to connect to. **port** Port number to connect to. **handler_factory** Class to instantiate to publish remote functions to the server. By default this is *NullHandler* which means that no functions are executable by the server. **(return value)** A *bjson.connection.Connection* instance or raises an exception. Connections are usually created this way:: import bjsonrpc conn = bjsonrpc.connect("rpc.host.net") print conn.call.some_method_in_server_side() """ sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sck.connect((host, port)) return bjsonrpc.connection.Connection(sck, handler_factory=handler_factory) bjsonrpc-0.2.0/bjsonrpc/proxies.py0000644000175000017500000000242411511304531016462 0ustar deaviddeavid""" bjson/proxies.py Copyright (c) 2010 David Martinez Marti All rights reserved. Licensed under 3-clause BSD License. See LICENSE.txt for the full license text. """ # import weakref class Proxy(object): """ Object that forwards calls to the remote. Parameters: **conn** Connection object to forward calls. **sync_type** synchronization type. 0-synchronous. 1-asynchronous. 2-notification. **obj** = None optional. Object name to call their functions, (used to proxy functions of *RemoteObject* """ def __init__(self, conn, sync_type, obj = None): self._conn = conn self._obj = obj self.sync_type = sync_type def __getattr__(self, name): if self._obj: name = "%s.%s" % (self._obj, name) def function(*args, **kwargs): """ Decorator-like function that forwards all calls to proxy method of connection. """ return self._conn.proxy(self.sync_type, name, args, kwargs) #print name function.__name__ = str(name) function._conn = self._conn # function.sync_type = self.sync_type return function bjsonrpc-0.2.0/bjsonrpc/request.py0000644000175000017500000001036411547364435016505 0ustar deaviddeavid""" bjson/request.py Asynchronous Bidirectional JSON-RPC protocol implementation over TCP/IP Copyright (c) 2010 David Martinez Marti All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ from threading import Event import traceback from bjsonrpc.exceptions import ServerError import bjsonrpc.jsonlib as json class Request(object): """ Represents a request to the other end which may be not be completed yet. This class is automatically created by *method* Proxy. Parameters: **conn** Connection instance which this Request belongs to. **request_data** Dictionary object to serialize as JSON to send to the other end. """ def __init__(self, conn, request_data): self.conn = conn self.data = request_data self.response = None self.event_response = Event() self.callbacks = [] self.thread_wait = self.event_response.wait self.request_id = None if 'id' in self.data: self.request_id = self.data['id'] if self.request_id: self.conn.addrequest(self) data = json.dumps(self.data, self.conn) self.conn.write(data) def setresponse(self, value): """ Method used by Connection instance to tell Request that a Response is available to this request. Parameters: **value** Value (JSON decoded) received from socket. """ self.response = value for callback in self.callbacks: try: callback(self) except Exception, exc: print "Error on callback.", repr(exc) print traceback.format_exc() self.event_response.set() # helper for threads. def wait(self): """ Block until there is a response. Will manage the socket and dispatch messages until the response is found. """ #if self.response is None: # self.conn.read_ensure_thread() while self.response is None: self.conn.read_and_dispatch(condition=lambda: self.response is None) @property def value(self): """ Get request value response. If the response is not available, it waits to it (see *wait* method). If the response contains an Error, this method raises *exceptions.ServerError* with the error text inside. """ self.wait() if self.response.get('error', None) is not None: raise ServerError(self.response['error']) return self.response['result'] bjsonrpc-0.2.0/bjsonrpc/server.py0000644000175000017500000001414111511412374016303 0ustar deaviddeavid""" bjson/server.py Asynchronous Bidirectional JSON-RPC protocol implementation over TCP/IP Copyright (c) 2010 David Martinez Marti All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ import socket, select from bjsonrpc.connection import Connection from bjsonrpc.exceptions import EofError class Server(object): """ Handles a listening socket and automatically accepts incoming connections. It will create a *bjsonrpc.connection.Connection* for each socket connected to it. Use the *Server.serve()* method to start accepting connections. Parameters: **lstsck** Listening socket to watch for incoming connections. Must be an instance of *socket.socket* or something compatible, and must to be already listening for new connections in the desired port. **handler_factory** Class (object type) to instantiate to publish methods for incoming connections. Should be an inherited class of *bjsonrpc.handlers.BaseHandler* """ def __init__(self, lstsck, handler_factory): self._lstsck = lstsck self._handler = handler_factory self._debug_socket = False self._debug_dispatch = False self._serve = True def stop(self): self._serve = False def debug_socket(self, value = None): """ Sets or retrieves the internal debug_socket value. When is set to true, each new connection will have it set to true, and every data sent or received by the socket will be printed to stdout. By default is set to *False* """ retval = self._debug_socket if type(value) is bool: self._debug_socket = value return retval def debug_dispatch(self, value = None): """ Sets or retrieves the internal debug_dispatch value. When is set to true, each new connection will have it set to true, and every error produced by client connections will be printed to stdout. By default is set to *False* """ ret = self._debug_dispatch if type(value) is bool: self._debug_dispatch = value return ret def serve(self): """ Starts the forever-serving loop. This function only exits when an Exception is raised inside, by unexpected error, KeyboardInterrput, etc. It is coded using *select.select* function, and it is capable to serve to an unlimited amount of connections at same time without using threading. """ try: sockets = [] connections = [] connidx = {} while self._serve: ready_to_read = select.select( [self._lstsck]+sockets, # read [], [], # write, errors 1 # timeout )[0] if not ready_to_read: continue if self._lstsck in ready_to_read: clientsck, clientaddr = self._lstsck.accept() sockets.append(clientsck) conn = Connection( sck = clientsck, address = clientaddr, handler_factory = self._handler ) connidx[clientsck.fileno()] = conn conn._debug_socket = self._debug_socket conn._debug_dispatch = self._debug_socket # conn.internal_error_callback = self. connections.append(conn) for sck in ready_to_read: fileno = sck.fileno() if fileno not in connidx: continue conn = connidx[fileno] try: conn.dispatch_until_empty() except EofError: conn.close() sockets.remove(conn.socket) connections.remove(conn) #print "Closing client conn." finally: for conn in connections: conn.close() self._lstsck.shutdown(socket.SHUT_RDWR) self._lstsck.close() bjsonrpc-0.2.0/test/0000755000175000017500000000000011554316615013571 5ustar deaviddeavidbjsonrpc-0.2.0/test/test_main.py0000644000175000017500000001207111511643363016123 0ustar deaviddeavidimport unittest import sys sys.path.insert(0, "../") import bjsonrpc from bjsonrpc.exceptions import ServerError import testserver1 import math from types import ListType class TestJSONBasics(unittest.TestCase): def setUp(self): """ Start the server and connect the client for every test. """ testserver1.start() self.conn = bjsonrpc.connect() def tearDown(self): """ Stop the server every test. """ testserver1.stop(self.conn) #def test_zzz_stopserver(self): # workaround for teardownclass in python <2.7 # testserver1.stop() def test_call(self): """ Call to remote without parameters """ rcall = self.conn.call for i in range(30): pong = rcall.ping() self.assertEqual(pong, "pong", "Server MUST return 'pong' when ping is called") def test_call2params(self): """ Call to remote with 2 parameters """ rcall = self.conn.call pairs = [ (941, -499), (1582, 1704), (-733, 119), (-967, 1755), (95, 286), (1866, -954), (495, -241), (966, 994), (1975, -758), (1939, 1699), (-42, 1018), (44, -346), (-621, -77), (-525, -804), (1537, 417), (1851, 1660), (-293, 1391), (-710, -796), (1990, -542), (-624, -115), ] for x, y in pairs: added = rcall.add2(x, y) self.assertEqual(added, x+y, "Server FAILED to add remotely the two params") def test_callNparams(self): """ Call to remote with N parameters """ rcall = self.conn.call for i in range(50): tosum = [ x + i * 3 + x % 10 for x in range(10+i*4)] added = rcall.addN(*tosum) self.assertEqual(added, sum(tosum), "Server FAILED to sum N params remotely") def test_float(self): """ Call to remote with float parameters """ rcall = self.conn.call precision = 10**6 for i in range(50): # make sure that the sum of this list is a float. Try to get complex floats. tosum = [ math.sin(x + i * 3 + x % 10) * 1999 / float(i+math.pi) for x in range(10+i*4)] added = rcall.addN(*tosum) self.assertNotEqual(added, int(added), "Test invalid! Should try only floats, not integers.") # Ok, this can be equal, but this test should fail in this case self.assertAlmostEqual(added, sum(tosum), 6, "Float precision error when doing a remote sum of params") def test_nested_list(self): """ Call with a nested list """ rcall = self.conn.call nlist = [] tot = 0 for i in range(10): ilist = [ (n% 3) *2 + n + i%2 + i * 4 for n in range(10)] tot += sum(ilist) nlist.append(ilist) result = rcall.addnlist(nlist) self.assertEqual(result, tot, "Failed to remotely sum elements of a nested list") def test_tuple(self): """ Tuples should be converted to lists """ rcall = self.conn.call result = rcall.getabc() self.assertTrue(isinstance(result, ListType),"JSON library should convert tuples to lists!") def test_kwparams(self): """ Call remote using keyword parameters """ rcall = self.conn.call result = rcall.getabc(a=1, b=2, c=3) self.assertEqual(result, [1, 2, 3]) result = rcall.getabc(b=1, c=2, a=3) self.assertEqual(result, [3, 1, 2]) result = rcall.getabc(b="b", c="c", a="a") self.assertEqual(result, ["a", "b", "c"]) result = rcall.getabc(c="c", b="b") self.assertEqual(result, [None, "b", "c"]) def test_commonerrors(self): rcall = self.conn.call self.assertRaises(ServerError, rcall.myfun) # inexistent method self.assertRaises(ServerError, rcall.add) # not enough parameters self.assertRaises(ServerError, rcall.getabc, j=32) # "j" parameter unknown self.assertRaises(ServerError, rcall.add, 2, 3, 4,) # too parameters def test_methodNparams(self): """ Get remote method for call with N parameters """ rmethod = self.conn.method lmethods = [] total = 0 for i in range(50): tosum = [ x + i * 3 + x % 10 for x in range(10+i*4)] total += sum(tosum) lmethods.append(rmethod.addN(*tosum)) remote_total = sum([ m.value for m in lmethods ]) self.assertEqual(total, remote_total, "Server FAILED to sum N params remotely handling paralell queries") if __name__ == '__main__': unittest.main() bjsonrpc-0.2.0/test/testserver1.py0000644000175000017500000000214211511640641016421 0ustar deaviddeavidfrom bjsonrpc.handlers import BaseHandler from bjsonrpc import createserver import threading class ServerHandler(BaseHandler): def ping(self): return "pong" def add2(self, num1 , num2): return num1 + num2 def addN(self, *args): return sum(args) def addnlist(self, nlist): tot = 0 for xlist in nlist: tot += sum(xlist) return tot def getabc(self, a=None, b=None, c=None): return (a, b, c) server = None def start(): global server, server_thread if server: return server = createserver(handler_factory=ServerHandler) server_thread = threading.Thread(target=server.serve) server_thread.daemon = True server_thread.start() def stop(c): global server, server_thread if server is None: return server.stop() try: c.notify.ping() except Exception: pass server_thread.join(timeout=5) if server_thread.is_alive(): raise IOError("Server Still Alive!!!") server = None server_thread = None bjsonrpc-0.2.0/README0000644000175000017500000000054111511304334013456 0ustar deaviddeavid bjsonrpc - asyncronous Bidirectional JSON-RPC implementation over TCP/IP. Inspired by previous similar works like IRPC (deavid.github.com/irpc), and a complete rewrite of jpc (http://code.google.com/p/python-jpc) (jpc author is Nir Aides ). Licensed over 3-clause BSD license. Author: David Martinez Marti bjsonrpc-0.2.0/setup.py0000644000175000017500000000562511554312771014333 0ustar deaviddeavid""" setup.py Copyright (c) 2010 David Martinez Marti All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ from distutils.core import setup setup( name = 'bjsonrpc', version = '0.2.0', description = 'Asynchronous Bidirectional JSON-RPC protocol implementation over TCP/IP', long_description = """ *bjsonrpc* is a implementation of the well-known JSON-RPC protocol over TCP-IP with lots of features. It is aimed at speed and simplicity and it adds some other extensions to JSON-RPC that makes *bjsonrpc* a very powerful tool as a IPC mechanism over low bandwith. """, author = 'David Martinez Marti', author_email = 'deavidsedice@gmail.com', url = 'http://deavid.github.com/bjsonrpc', download_url = 'http://github.com/deavid/bjsonrpc/tarball/master', classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: DFSG approved", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Topic :: Communications", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Object Brokering", ], packages = ['bjsonrpc'] ) bjsonrpc-0.2.0/PKG-INFO0000644000175000017500000000237711554316676013727 0ustar deaviddeavidMetadata-Version: 1.0 Name: bjsonrpc Version: 0.2.0 Summary: Asynchronous Bidirectional JSON-RPC protocol implementation over TCP/IP Home-page: http://deavid.github.com/bjsonrpc Author: David Martinez Marti Author-email: deavidsedice@gmail.com License: UNKNOWN Download-URL: http://github.com/deavid/bjsonrpc/tarball/master Description: *bjsonrpc* is a implementation of the well-known JSON-RPC protocol over TCP-IP with lots of features. It is aimed at speed and simplicity and it adds some other extensions to JSON-RPC that makes *bjsonrpc* a very powerful tool as a IPC mechanism over low bandwith. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: DFSG approved Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Topic :: Communications Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Object Brokering