tornadorpc-0.1.1/0000755000076500000240000000000012310673650014767 5ustar jmarshallstaff00000000000000tornadorpc-0.1.1/PKG-INFO0000644000076500000240000002376212310673650016076 0ustar jmarshallstaff00000000000000Metadata-Version: 1.0 Name: tornadorpc Version: 0.1.1 Summary: TornadoRPC is a an implementation of both JSON-RPC and XML-RPC handlers for the Tornado framework. Home-page: http://code.google.com/p/tornadorpc/ Author: Josh Marshall Author-email: catchjosh@gmail.com License: http://www.apache.org/licenses/LICENSE-2.0 Description: [![Build Status](https://travis-ci.org/joshmarshall/tornadorpc.png?branch=master)](https://travis-ci.org/joshmarshall/tornadorpc) TORNADO-RPC =========== This library is an implementation of both the JSON-RPC and the XML-RPC specification (server-side) for the Tornado web framework. It supports the basic features of both, as well as the MultiCall / Batch support for both specifications. The JSON-RPC handler supports both the original 1.0 specification, as well as the new (proposed) 2.0 spec, which includes batch submission, keyword arguments, etc. Asynchronous request support has been added for methods which require the use of asynchronous libraries (like Tornado's AsyncHTTPClient library.) TornadoRPC is licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html). Mailing List ------------ If you have any questions, issues, or just use the library please feel free to send a message to the mailing list at: http://groups.google.com/group/tornadorpc Installation ------------ To install: python setup.py build sudo python setup.py install To use this module, you'll need Tornado installed, which you can get at this address: http://www.tornadoweb.org/ If you want to use the JSON-RPC handler, you'll also need jsonrpclib, which you can grab at: http://github.com/joshmarshall/jsonrpclib/ The jsonrpclib library requires one of the JSON libraries. It looks first for cjson, then for the built-in JSON library (with default Python 2.6+ distributions), and finally the simplejson library. Overview -------- This library is an implementation of both the JSON-RPC and the XML-RPC specification (server-side) for the Tornado web framework. It supports the basic features of both, as well as the MultiCall / Batch support for both specifications. The JSON-RPC handler supports both the original 1.0 specification, as well as the new (proposed) 2.0 spec, which includes batch submission, keyword arguments, etc. There is also a base library that other RPC protocols could use to quickly get tied into Tornado. Requirements ------------ The library obviously requires Tornado, which you can get at Tornado's website (http://www.tornadoweb.org). After installing Tornado (instructions included with the Tornado distribution) you should be able to use the XML-RPC handler without any other libraries. The JSON-RPC handler requires my jsonrpclib library, which you can get at http://github.com/joshmarshall/jsonrpclib . It also requires a JSON library, although any distribution of Python past 2.5 should have it by default. (Note: Some Linuxes only include a base Python install. On Ubuntu, for instance, you may need to run `sudo apt-get install python-json` or `sudo apt-get python-cjson` to get one of the libraries.) Usage ----- The library is designed to be mostly transparent in usage. You simply extend the XML/JSON RPCHandler class from either the tornadorpc.xml or the tornado.json library, resepectively, and pass that handler in to the Tornado framework just like any other handler. For any synchronous (normal) operation, you can just return the value you want sent to the client. However, if you use any asynchronous library (like Tornado's AsyncHTTPClient) you will want to call self.result(RESULT) in your callback. See the Asynchronous section below for examples. XML-RPC Example --------------- To set up a simple XML RPC server, this is all you need: from tornadorpc.xml import XMLRPCHandler from tornadorpc import private, start_server class Handler(XMLRPCHandler): def add(self, x, y): return x+y def ping(self, obj): return obj @private def private(self): #should not get called return False start_server(Handler, port=8080) The `@private` decorator is a way to ensure that it cannot be called externally. You can also create methods that start with an underscore `_` character, and they will be private by default. The `start_server` function is just an easy wrap around the default Tornado setup -- you can use these handlers just like you would any other Tornado RequestHandler. JSON-RPC Example ---------------- A JSON-RPC server would be started with the exact same syntax, replacing XMLRPCHandler with JSONRPCHandler. Here is an example of the JSON-RPC client with "dot-attribute" support: from tornadorpc.json import JSONRPCHandler from tornadorpc import private, start_server class Tree(object): def power(self, base, power, modulo=None): result = pow(base, power, modulo) return result def _private(self): # Won't be callable return False class Handler(JSONRPCHandler): tree = Tree() def add(self, x, y): return x+y def ping(self, obj): return obj start_server(Handler, port=8080) To use this, you should be able to use either the JSON-RPC official implementation, or the jsonrpclib library (which you'd need for this to work anyway.) One of the benefits of the jsonrpclib is designed to be a parallel implementation to the xmlrpclib, so syntax should be very similar and it should be easy to experiment with existing apps. An example of client usage would be: from jsonrpclib import Server server = Server('http://localhost:8080') result = server.tree.power(2, 6) # result should equal 64 Asynchronous Example -------------------- To indicate that a request is asynchronous, simply use the "async" decorator, and call "self.result(RESULT)" in your callback. Please note that this will only work in the RPCHandler methods, not in any sub-tree methods since they do not have access to the handler's result() method. Here is an example that uses Tornado's AsyncHTTPClient with a callback: from tornadorpc import async from tornadorpc.xml import XMLRPCHandler from tornado.httpclient import AsyncHTTPClient class Handler(XMLRPCHandler): @async def external(self, url): client = AsyncHTTPClient() client.fetch(url, self._handle_response) def _handle_response(self, response): # The underscore will make it private automatically # You could also use @private if you wished # This returns the status code of the request self.result(response.code) Debugging --------- There is a `config` object that is available -- it will be expanded as time goes by. Currently, it supports two options: `verbose` and `short_errors`, both of which default to True. The `verbose` setting just specifies whether you want to print out results to the terminal (automatically on, you'll probably want to turn that off for production, WSGI deployment, etc.) and the `short_errors` option determines whether to print just the last few lines of the traceback (if set to True, default) or print the full traceback. Once the logging mechanism is in place, the `short_errors` configuration element will apply to that as well. The default error look something similar to this: JSON-RPC SERVER AT http://localhost:8484 --------------- ERROR IN messup --------------- Traceback (most recent call last): File "test.py", line 20, in messup return doesntexist['bad_key'] NameError: global name 'doesntexist' is not defined To change the configuration, look over the following: import tornadorpc tornadorpc.config.verbose = False tornadorpc.config.short_errors = False # or... from tornadorpc import config config.verbose = False config.short_errors = False Tests ----- To run some basic tests, enter the following in the same directory that this README is in: python run_tests.py This will test a few basic utilites and the XMLRPC system. If you wish to test the JSONRPC system, run the following: python run_tests.py --json TODO ---- * Add unit tests * Add logging mechanism * Add proper HTTP codes for failures * Optimize Platform: UNKNOWN tornadorpc-0.1.1/README0000644000076500000240000001751412310673650015657 0ustar jmarshallstaff00000000000000[![Build Status](https://travis-ci.org/joshmarshall/tornadorpc.png?branch=master)](https://travis-ci.org/joshmarshall/tornadorpc) TORNADO-RPC =========== This library is an implementation of both the JSON-RPC and the XML-RPC specification (server-side) for the Tornado web framework. It supports the basic features of both, as well as the MultiCall / Batch support for both specifications. The JSON-RPC handler supports both the original 1.0 specification, as well as the new (proposed) 2.0 spec, which includes batch submission, keyword arguments, etc. Asynchronous request support has been added for methods which require the use of asynchronous libraries (like Tornado's AsyncHTTPClient library.) TornadoRPC is licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html). Mailing List ------------ If you have any questions, issues, or just use the library please feel free to send a message to the mailing list at: http://groups.google.com/group/tornadorpc Installation ------------ To install: python setup.py build sudo python setup.py install To use this module, you'll need Tornado installed, which you can get at this address: http://www.tornadoweb.org/ If you want to use the JSON-RPC handler, you'll also need jsonrpclib, which you can grab at: http://github.com/joshmarshall/jsonrpclib/ The jsonrpclib library requires one of the JSON libraries. It looks first for cjson, then for the built-in JSON library (with default Python 2.6+ distributions), and finally the simplejson library. Overview -------- This library is an implementation of both the JSON-RPC and the XML-RPC specification (server-side) for the Tornado web framework. It supports the basic features of both, as well as the MultiCall / Batch support for both specifications. The JSON-RPC handler supports both the original 1.0 specification, as well as the new (proposed) 2.0 spec, which includes batch submission, keyword arguments, etc. There is also a base library that other RPC protocols could use to quickly get tied into Tornado. Requirements ------------ The library obviously requires Tornado, which you can get at Tornado's website (http://www.tornadoweb.org). After installing Tornado (instructions included with the Tornado distribution) you should be able to use the XML-RPC handler without any other libraries. The JSON-RPC handler requires my jsonrpclib library, which you can get at http://github.com/joshmarshall/jsonrpclib . It also requires a JSON library, although any distribution of Python past 2.5 should have it by default. (Note: Some Linuxes only include a base Python install. On Ubuntu, for instance, you may need to run `sudo apt-get install python-json` or `sudo apt-get python-cjson` to get one of the libraries.) Usage ----- The library is designed to be mostly transparent in usage. You simply extend the XML/JSON RPCHandler class from either the tornadorpc.xml or the tornado.json library, resepectively, and pass that handler in to the Tornado framework just like any other handler. For any synchronous (normal) operation, you can just return the value you want sent to the client. However, if you use any asynchronous library (like Tornado's AsyncHTTPClient) you will want to call self.result(RESULT) in your callback. See the Asynchronous section below for examples. XML-RPC Example --------------- To set up a simple XML RPC server, this is all you need: from tornadorpc.xml import XMLRPCHandler from tornadorpc import private, start_server class Handler(XMLRPCHandler): def add(self, x, y): return x+y def ping(self, obj): return obj @private def private(self): #should not get called return False start_server(Handler, port=8080) The `@private` decorator is a way to ensure that it cannot be called externally. You can also create methods that start with an underscore `_` character, and they will be private by default. The `start_server` function is just an easy wrap around the default Tornado setup -- you can use these handlers just like you would any other Tornado RequestHandler. JSON-RPC Example ---------------- A JSON-RPC server would be started with the exact same syntax, replacing XMLRPCHandler with JSONRPCHandler. Here is an example of the JSON-RPC client with "dot-attribute" support: from tornadorpc.json import JSONRPCHandler from tornadorpc import private, start_server class Tree(object): def power(self, base, power, modulo=None): result = pow(base, power, modulo) return result def _private(self): # Won't be callable return False class Handler(JSONRPCHandler): tree = Tree() def add(self, x, y): return x+y def ping(self, obj): return obj start_server(Handler, port=8080) To use this, you should be able to use either the JSON-RPC official implementation, or the jsonrpclib library (which you'd need for this to work anyway.) One of the benefits of the jsonrpclib is designed to be a parallel implementation to the xmlrpclib, so syntax should be very similar and it should be easy to experiment with existing apps. An example of client usage would be: from jsonrpclib import Server server = Server('http://localhost:8080') result = server.tree.power(2, 6) # result should equal 64 Asynchronous Example -------------------- To indicate that a request is asynchronous, simply use the "async" decorator, and call "self.result(RESULT)" in your callback. Please note that this will only work in the RPCHandler methods, not in any sub-tree methods since they do not have access to the handler's result() method. Here is an example that uses Tornado's AsyncHTTPClient with a callback: from tornadorpc import async from tornadorpc.xml import XMLRPCHandler from tornado.httpclient import AsyncHTTPClient class Handler(XMLRPCHandler): @async def external(self, url): client = AsyncHTTPClient() client.fetch(url, self._handle_response) def _handle_response(self, response): # The underscore will make it private automatically # You could also use @private if you wished # This returns the status code of the request self.result(response.code) Debugging --------- There is a `config` object that is available -- it will be expanded as time goes by. Currently, it supports two options: `verbose` and `short_errors`, both of which default to True. The `verbose` setting just specifies whether you want to print out results to the terminal (automatically on, you'll probably want to turn that off for production, WSGI deployment, etc.) and the `short_errors` option determines whether to print just the last few lines of the traceback (if set to True, default) or print the full traceback. Once the logging mechanism is in place, the `short_errors` configuration element will apply to that as well. The default error look something similar to this: JSON-RPC SERVER AT http://localhost:8484 --------------- ERROR IN messup --------------- Traceback (most recent call last): File "test.py", line 20, in messup return doesntexist['bad_key'] NameError: global name 'doesntexist' is not defined To change the configuration, look over the following: import tornadorpc tornadorpc.config.verbose = False tornadorpc.config.short_errors = False # or... from tornadorpc import config config.verbose = False config.short_errors = False Tests ----- To run some basic tests, enter the following in the same directory that this README is in: python run_tests.py This will test a few basic utilites and the XMLRPC system. If you wish to test the JSONRPC system, run the following: python run_tests.py --json TODO ---- * Add unit tests * Add logging mechanism * Add proper HTTP codes for failures * Optimize tornadorpc-0.1.1/setup.cfg0000644000076500000240000000007312310673650016610 0ustar jmarshallstaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 tornadorpc-0.1.1/setup.py0000644000076500000240000000142212310673520016474 0ustar jmarshallstaff00000000000000#!/usr/bin/env/python import os import setuptools readme_path = "README.md" if not os.path.exists(readme_path): readme_path = "README" with open(readme_path) as readme: long_description = readme.read() with open("README", "w") as pypi_readme: pypi_readme.write(long_description) setuptools.setup( name="tornadorpc", version="0.1.1", packages=["tornadorpc"], author="Josh Marshall", install_requires=["tornado", "jsonrpclib"], author_email="catchjosh@gmail.com", url="http://code.google.com/p/tornadorpc/", license="http://www.apache.org/licenses/LICENSE-2.0", description="TornadoRPC is a an implementation of both JSON-RPC " "and XML-RPC handlers for the Tornado framework.", long_description=long_description ) tornadorpc-0.1.1/tornadorpc/0000755000076500000240000000000012310673650017142 5ustar jmarshallstaff00000000000000tornadorpc-0.1.1/tornadorpc/__init__.py0000644000076500000240000000116012270772624021257 0ustar jmarshallstaff00000000000000""" Copyright 2009 Josh Marshall Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from base import private, async, start_server, config tornadorpc-0.1.1/tornadorpc/base.py0000644000076500000240000003277612271027342020442 0ustar jmarshallstaff00000000000000""" ============================ Base RPC Handler for Tornado ============================ This is a basic server implementation, designed for use within the Tornado framework. The classes in this library should not be used directly, but rather though the XML or JSON RPC implementations. You can use the utility functions like 'private' and 'start_server'. """ from tornado.web import RequestHandler import tornado.web import tornado.ioloop import tornado.httpserver import types import traceback from tornadorpc.utils import getcallargs # Configuration element class Config(object): verbose = True short_errors = True config = Config() class BaseRPCParser(object): """ This class is responsible for managing the request, dispatch, and response formatting of the system. It is tied into the _RPC_ attribute of the BaseRPCHandler (or subclasses) and populated as necessary throughout the request. Use the .faults attribute to take advantage of the built-in error codes. """ content_type = 'text/plain' def __init__(self, library, encode=None, decode=None): # Attaches the RPC library and encode / decode functions. self.library = library if not encode: encode = getattr(library, 'dumps') if not decode: decode = getattr(library, 'loads') self.encode = encode self.decode = decode self.requests_in_progress = 0 self.responses = [] @property def faults(self): # Grabs the fault tree on request return Faults(self) def run(self, handler, request_body): """ This is the main loop -- it passes the request body to the parse_request method, and then takes the resulting method(s) and parameters and passes them to the appropriate method on the parent Handler class, then parses the response into text and returns it to the parent Handler to send back to the client. """ self.handler = handler try: requests = self.parse_request(request_body) except: self.traceback() return self.handler.result(self.faults.parse_error()) if not isinstance(requests, types.TupleType): # SHOULD be the result of a fault call, # according tothe parse_request spec below. if isinstance(requests, basestring): # Should be the response text of a fault # This will break in Python 3.x return requests elif hasattr(requests, 'response'): # Fault types should have a 'response' method return requests.response() elif hasattr(requests, 'faultCode'): # XML-RPC fault types need to be properly dispatched. This # should only happen if there was an error parsing the # request above. return self.handler.result(requests) else: # No idea, hopefully the handler knows what it # is doing. return requests self.handler._requests = len(requests) for request in requests: self.dispatch(request[0], request[1]) def dispatch(self, method_name, params): """ This method walks the attribute tree in the method and passes the parameters, either in positional or keyword form, into the appropriate method on the Handler class. Currently supports only positional or keyword arguments, not mixed. """ if hasattr(RequestHandler, method_name): # Pre-existing, not an implemented attribute return self.handler.result(self.faults.method_not_found()) method = self.handler method_list = dir(method) method_list.sort() attr_tree = method_name.split('.') try: for attr_name in attr_tree: method = self.check_method(attr_name, method) except AttributeError: return self.handler.result(self.faults.method_not_found()) if not callable(method): # Not callable, so not a method return self.handler.result(self.faults.method_not_found()) if method_name.startswith('_') or \ getattr(method, 'private', False) is True: # No, no. That's private. return self.handler.result(self.faults.method_not_found()) args = [] kwargs = {} if isinstance(params, dict): # The parameters are keyword-based kwargs = params elif type(params) in (list, tuple): # The parameters are positional args = params else: # Bad argument formatting? return self.handler.result(self.faults.invalid_params()) # Validating call arguments try: final_kwargs, extra_args = getcallargs(method, *args, **kwargs) except TypeError: return self.handler.result(self.faults.invalid_params()) try: response = method(*extra_args, **final_kwargs) except Exception: self.traceback(method_name, params) return self.handler.result(self.faults.internal_error()) if getattr(method, 'async', False): # Asynchronous response -- the method should have called # self.result(RESULT_VALUE) if response is not None: # This should be deprecated to use self.result return self.handler.result(self.faults.internal_error()) else: # Synchronous result -- we call result manually. return self.handler.result(response) def response(self, handler): """ This is the callback for a single finished dispatch. Once all the dispatches have been run, it calls the parser library to parse responses and then calls the handler's async method. """ handler._requests -= 1 if handler._requests > 0: return # We are finished with requests, send response if handler._RPC_finished: # We've already sent the response raise Exception("Error trying to send response twice.") handler._RPC_finished = True responses = tuple(handler._results) response_text = self.parse_responses(responses) if type(response_text) not in types.StringTypes: # Likely a fault, or something messed up response_text = self.encode(response_text) # Calling the async callback handler.on_result(response_text) def traceback(self, method_name='REQUEST', params=[]): err_lines = traceback.format_exc().splitlines() err_title = "ERROR IN %s" % method_name if len(params) > 0: err_title = '%s - (PARAMS: %s)' % (err_title, repr(params)) err_sep = ('-'*len(err_title))[:79] err_lines = [err_sep, err_title, err_sep]+err_lines if config.verbose: if len(err_lines) >= 7 and config.short_errors: # Minimum number of lines to see what happened # Plus title and separators print '\n'.join(err_lines[0:4]+err_lines[-3:]) else: print '\n'.join(err_lines) # Log here return def parse_request(self, request_body): """ Extend this on the implementing protocol. If it should error out, return the output of the 'self.faults.fault_name' response. Otherwise, it MUST return a TUPLE of TUPLE. Each entry tuple must have the following structure: ('method_name', params) ...where params is a list or dictionary of arguments (positional or keyword, respectively.) So, the result should look something like the following: ( ('add', [5,4]), ('add', {'x':5, 'y':4}) ) """ return ([], []) def parse_responses(self, responses): """ Extend this on the implementing protocol. It must return a response that can be returned as output to the client. """ return self.encode(responses, methodresponse=True) def check_method(self, attr_name, obj): """ Just checks to see whether an attribute is private (by the decorator or by a leading underscore) and returns boolean result. """ if attr_name.startswith('_'): raise AttributeError('Private object or method.') attr = getattr(obj, attr_name) if getattr(attr, 'private', False): raise AttributeError('Private object or method.') return attr class BaseRPCHandler(RequestHandler): """ This is the base handler to be subclassed by the actual implementations and by the end user. """ _RPC_ = None _results = None _requests = 0 _RPC_finished = False @tornado.web.asynchronous def post(self): # Very simple -- dispatches request body to the parser # and returns the output self._results = [] request_body = self.request.body self._RPC_.run(self, request_body) def result(self, result, *results): """ Use this to return a result. """ if results: results = [result] + results else: results = result self._results.append(results) self._RPC_.response(self) def on_result(self, response_text): """ Asynchronous callback. """ self.set_header('Content-Type', self._RPC_.content_type) self.finish(response_text) class FaultMethod(object): """ This is the 'dynamic' fault method so that the message can be changed on request from the parser.faults call. """ def __init__(self, fault, code, message): self.fault = fault self.code = code self.message = message def __call__(self, message=None): if message: self.message = message return self.fault(self.code, self.message) class Faults(object): """ This holds the codes and messages for the RPC implementation. It is attached (dynamically) to the Parser when called via the parser.faults query, and returns a FaultMethod to be called so that the message can be changed. If the 'dynamic' attribute is not a key in the codes list, then it will error. USAGE: parser.fault.parse_error('Error parsing content.') If no message is passed in, it will check the messages dictionary for the same key as the codes dict. Otherwise, it just prettifies the code 'key' from the codes dict. """ codes = { 'parse_error': -32700, 'method_not_found': -32601, 'invalid_request': -32600, 'invalid_params': -32602, 'internal_error': -32603 } messages = {} def __init__(self, parser, fault=None): self.library = parser.library self.fault = fault if not self.fault: self.fault = getattr(self.library, 'Fault') def __getattr__(self, attr): message = 'Error' if attr in self.messages.keys(): message = self.messages[attr] else: message = ' '.join(map(str.capitalize, attr.split('_'))) fault = FaultMethod(self.fault, self.codes[attr], message) return fault """ Utility Functions """ def private(func): """ Use this to make a method private. It is intended to be used as a decorator. If you wish to make a method tree private, just create and set the 'private' variable to True on the tree object itself. """ func.private = True return func def async(func): """ Use this to make a method asynchronous It is intended to be used as a decorator. Make sure you call "self.result" on any async method. Also, trees do not currently support async methods. """ func.async = True return func def start_server(handlers, route=r'/', port=8080): """ This is just a friendly wrapper around the default Tornado instantiation calls. It simplifies the imports and setup calls you'd make otherwise. USAGE: start_server(handler_class, route=r'/', port=8181) """ if type(handlers) not in (types.ListType, types.TupleType): handler = handlers handlers = [(route, handler)] if route != '/RPC2': # friendly addition for /RPC2 if it's the only one handlers.append(('/RPC2', handler)) application = tornado.web.Application(handlers) http_server = tornado.httpserver.HTTPServer(application) http_server.listen(port) loop_instance = tornado.ioloop.IOLoop.instance() """ Setting the '_server' attribute if not set """ for (route, handler) in handlers: try: setattr(handler, '_server', loop_instance) except AttributeError: handler._server = loop_instance loop_instance.start() return loop_instance """ The following is a test implementation which should work for both the XMLRPC and the JSONRPC clients. """ class TestMethodTree(object): def power(self, x, y=2): return pow(x, y) @private def private(self): # Shouldn't be called return False class TestRPCHandler(BaseRPCHandler): _RPC_ = None def add(self, x, y): return x+y def ping(self, x): return x def noargs(self): return 'Works!' tree = TestMethodTree() def _private(self): # Shouldn't be called return False @private def private(self): # Also shouldn't be called return False tornadorpc-0.1.1/tornadorpc/json.py0000644000076500000240000000742612270776344020507 0ustar jmarshallstaff00000000000000""" ============================ JSON-RPC Handler for Tornado ============================ This is a JSON-RPC server implementation, designed for use within the Tornado framework. Usage is pretty simple: >>> from tornadorpc.json import JSONRPCHandler >>> from tornadorpc import start_server >>> >>> class handler(JSONRPCHandler): >>> ... def add(self, x, y): >>> ....... return x+y >>> >>> start_server(handler, port=8484) It requires the jsonrpclib, which you can get from: http://github.com/joshmarshall/jsonrpclib Also, you will need one of the following JSON modules: * cjson * simplejson From Python 2.6 on, simplejson is included in the standard distribution as the "json" module. """ from tornadorpc.base import BaseRPCParser, BaseRPCHandler import jsonrpclib from jsonrpclib.jsonrpc import isbatch, isnotification, Fault from jsonrpclib.jsonrpc import dumps, loads class JSONRPCParser(BaseRPCParser): content_type = 'application/json-rpc' def parse_request(self, request_body): try: request = loads(request_body) except: # Bad request formatting self.traceback() return self.faults.parse_error() self._requests = request self._batch = False request_list = [] if isbatch(request): self._batch = True for req in request: req_tuple = (req['method'], req.get('params', [])) request_list.append(req_tuple) else: self._requests = [request] request_list.append( (request['method'], request.get('params', [])) ) return tuple(request_list) def parse_responses(self, responses): if isinstance(responses, Fault): return dumps(responses) if len(responses) != len(self._requests): return dumps(self.faults.internal_error()) response_list = [] for i in range(0, len(responses)): request = self._requests[i] response = responses[i] if isnotification(request): # Even in batches, notifications have no # response entry continue rpcid = request['id'] version = jsonrpclib.config.version if 'jsonrpc' not in request.keys(): version = 1.0 try: response_json = dumps( response, version=version, rpcid=rpcid, methodresponse=True ) except TypeError: return dumps( self.faults.server_error(), rpcid=rpcid, version=version ) response_list.append(response_json) if not self._batch: # Ensure it wasn't a batch to begin with, then # return 1 or 0 responses depending on if it was # a notification. if len(response_list) < 1: return '' return response_list[0] # Batch, return list return '[ %s ]' % ', '.join(response_list) class JSONRPCLibraryWrapper(object): dumps = dumps loads = loads Fault = Fault class JSONRPCHandler(BaseRPCHandler): """ Subclass this to add methods -- you can treat them just like normal methods, this handles the JSON formatting. """ _RPC_ = JSONRPCParser(JSONRPCLibraryWrapper) if __name__ == '__main__': # Example Implementation import sys from tornadorpc.base import start_server from tornadorpc.base import TestRPCHandler class TestJSONRPC(TestRPCHandler): _RPC_ = JSONRPCParser(JSONRPCLibraryWrapper) port = 8181 if len(sys.argv) > 1: port = int(sys.argv[1]) print 'Starting server on port %s' % port start_server(TestJSONRPC, port=port) tornadorpc-0.1.1/tornadorpc/utils.py0000644000076500000240000000354512270776633020675 0ustar jmarshallstaff00000000000000""" Various utilities for the TornadoRPC library. """ import inspect def getcallargs(func, *positional, **named): """ Simple implementation of inspect.getcallargs function in the Python 2.7 standard library. Takes a function and the position and keyword arguments and returns a dictionary with the appropriate named arguments. Raises an exception if invalid arguments are passed. """ args, varargs, varkw, defaults = inspect.getargspec(func) final_kwargs = {} extra_args = [] has_self = inspect.ismethod(func) and func.im_self is not None if has_self: args.pop(0) # (Since our RPC supports only positional OR named.) if named: for key, value in named.iteritems(): arg_key = None try: arg_key = args[args.index(key)] except ValueError: if not varkw: raise TypeError("Keyword argument '%s' not valid" % key) if key in final_kwargs.keys(): message = "Keyword argument '%s' used more than once" % key raise TypeError(message) final_kwargs[key] = value else: for i in range(len(positional)): value = positional[i] arg_key = None try: arg_key = args[i] except IndexError: if not varargs: raise TypeError("Too many positional arguments") if arg_key: final_kwargs[arg_key] = value else: extra_args.append(value) if defaults: for kwarg, default in zip(args[-len(defaults):], defaults): final_kwargs.setdefault(kwarg, default) for arg in args: if arg not in final_kwargs: raise TypeError("Not all arguments supplied. (%s)", arg) return final_kwargs, extra_args tornadorpc-0.1.1/tornadorpc/xml.py0000644000076500000240000000456012270776473020335 0ustar jmarshallstaff00000000000000""" =========================== XML-RPC Handler for Tornado =========================== This is a XML-RPC server implementation, designed for use within the Tornado framework. Usage is pretty simple: >>> from tornadorpc.xml import XMLRPCHandler >>> from tornadorpc import start_server >>> >>> class handler(XMLRPCHandler): >>> ... def add(self, x, y): >>> ....... return x+y >>> >>> start_server(handler, port=8484) It requires the xmlrpclib, which is built-in to Python distributions from version 2.3 on. """ from tornadorpc.base import BaseRPCParser, BaseRPCHandler import xmlrpclib class XMLRPCSystem(object): # Multicall functions and, eventually, introspection def __init__(self, handler): self._dispatch = handler._RPC_.dispatch def multicall(self, calls): for call in calls: method_name = call['methodName'] params = call['params'] self._dispatch(method_name, params) class XMLRPCParser(BaseRPCParser): content_type = 'text/xml' def parse_request(self, request_body): try: params, method_name = xmlrpclib.loads(request_body) except: # Bad request formatting, bad. return self.faults.parse_error() return ((method_name, params),) def parse_responses(self, responses): try: if isinstance(responses[0], xmlrpclib.Fault): return xmlrpclib.dumps(responses[0]) except IndexError: pass try: response_xml = xmlrpclib.dumps(responses, methodresponse=True) except TypeError: return self.faults.internal_error() return response_xml class XMLRPCHandler(BaseRPCHandler): """ Subclass this to add methods -- you can treat them just like normal methods, this handles the XML formatting. """ _RPC_ = XMLRPCParser(xmlrpclib) @property def system(self): return XMLRPCSystem(self) if __name__ == '__main__': # Test implementation from tornadorpc.base import TestRPCHandler, start_server import sys port = 8282 if len(sys.argv) > 1: port = int(sys.argv[1]) class TestXMLRPC(TestRPCHandler): _RPC_ = XMLRPCParser(xmlrpclib) @property def system(self): return XMLRPCSystem(self) print 'Starting server on port %s' % port start_server(TestXMLRPC, port=port) tornadorpc-0.1.1/tornadorpc.egg-info/0000755000076500000240000000000012310673650020634 5ustar jmarshallstaff00000000000000tornadorpc-0.1.1/tornadorpc.egg-info/dependency_links.txt0000644000076500000240000000000112310673650024702 0ustar jmarshallstaff00000000000000 tornadorpc-0.1.1/tornadorpc.egg-info/PKG-INFO0000644000076500000240000002376212310673650021743 0ustar jmarshallstaff00000000000000Metadata-Version: 1.0 Name: tornadorpc Version: 0.1.1 Summary: TornadoRPC is a an implementation of both JSON-RPC and XML-RPC handlers for the Tornado framework. Home-page: http://code.google.com/p/tornadorpc/ Author: Josh Marshall Author-email: catchjosh@gmail.com License: http://www.apache.org/licenses/LICENSE-2.0 Description: [![Build Status](https://travis-ci.org/joshmarshall/tornadorpc.png?branch=master)](https://travis-ci.org/joshmarshall/tornadorpc) TORNADO-RPC =========== This library is an implementation of both the JSON-RPC and the XML-RPC specification (server-side) for the Tornado web framework. It supports the basic features of both, as well as the MultiCall / Batch support for both specifications. The JSON-RPC handler supports both the original 1.0 specification, as well as the new (proposed) 2.0 spec, which includes batch submission, keyword arguments, etc. Asynchronous request support has been added for methods which require the use of asynchronous libraries (like Tornado's AsyncHTTPClient library.) TornadoRPC is licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html). Mailing List ------------ If you have any questions, issues, or just use the library please feel free to send a message to the mailing list at: http://groups.google.com/group/tornadorpc Installation ------------ To install: python setup.py build sudo python setup.py install To use this module, you'll need Tornado installed, which you can get at this address: http://www.tornadoweb.org/ If you want to use the JSON-RPC handler, you'll also need jsonrpclib, which you can grab at: http://github.com/joshmarshall/jsonrpclib/ The jsonrpclib library requires one of the JSON libraries. It looks first for cjson, then for the built-in JSON library (with default Python 2.6+ distributions), and finally the simplejson library. Overview -------- This library is an implementation of both the JSON-RPC and the XML-RPC specification (server-side) for the Tornado web framework. It supports the basic features of both, as well as the MultiCall / Batch support for both specifications. The JSON-RPC handler supports both the original 1.0 specification, as well as the new (proposed) 2.0 spec, which includes batch submission, keyword arguments, etc. There is also a base library that other RPC protocols could use to quickly get tied into Tornado. Requirements ------------ The library obviously requires Tornado, which you can get at Tornado's website (http://www.tornadoweb.org). After installing Tornado (instructions included with the Tornado distribution) you should be able to use the XML-RPC handler without any other libraries. The JSON-RPC handler requires my jsonrpclib library, which you can get at http://github.com/joshmarshall/jsonrpclib . It also requires a JSON library, although any distribution of Python past 2.5 should have it by default. (Note: Some Linuxes only include a base Python install. On Ubuntu, for instance, you may need to run `sudo apt-get install python-json` or `sudo apt-get python-cjson` to get one of the libraries.) Usage ----- The library is designed to be mostly transparent in usage. You simply extend the XML/JSON RPCHandler class from either the tornadorpc.xml or the tornado.json library, resepectively, and pass that handler in to the Tornado framework just like any other handler. For any synchronous (normal) operation, you can just return the value you want sent to the client. However, if you use any asynchronous library (like Tornado's AsyncHTTPClient) you will want to call self.result(RESULT) in your callback. See the Asynchronous section below for examples. XML-RPC Example --------------- To set up a simple XML RPC server, this is all you need: from tornadorpc.xml import XMLRPCHandler from tornadorpc import private, start_server class Handler(XMLRPCHandler): def add(self, x, y): return x+y def ping(self, obj): return obj @private def private(self): #should not get called return False start_server(Handler, port=8080) The `@private` decorator is a way to ensure that it cannot be called externally. You can also create methods that start with an underscore `_` character, and they will be private by default. The `start_server` function is just an easy wrap around the default Tornado setup -- you can use these handlers just like you would any other Tornado RequestHandler. JSON-RPC Example ---------------- A JSON-RPC server would be started with the exact same syntax, replacing XMLRPCHandler with JSONRPCHandler. Here is an example of the JSON-RPC client with "dot-attribute" support: from tornadorpc.json import JSONRPCHandler from tornadorpc import private, start_server class Tree(object): def power(self, base, power, modulo=None): result = pow(base, power, modulo) return result def _private(self): # Won't be callable return False class Handler(JSONRPCHandler): tree = Tree() def add(self, x, y): return x+y def ping(self, obj): return obj start_server(Handler, port=8080) To use this, you should be able to use either the JSON-RPC official implementation, or the jsonrpclib library (which you'd need for this to work anyway.) One of the benefits of the jsonrpclib is designed to be a parallel implementation to the xmlrpclib, so syntax should be very similar and it should be easy to experiment with existing apps. An example of client usage would be: from jsonrpclib import Server server = Server('http://localhost:8080') result = server.tree.power(2, 6) # result should equal 64 Asynchronous Example -------------------- To indicate that a request is asynchronous, simply use the "async" decorator, and call "self.result(RESULT)" in your callback. Please note that this will only work in the RPCHandler methods, not in any sub-tree methods since they do not have access to the handler's result() method. Here is an example that uses Tornado's AsyncHTTPClient with a callback: from tornadorpc import async from tornadorpc.xml import XMLRPCHandler from tornado.httpclient import AsyncHTTPClient class Handler(XMLRPCHandler): @async def external(self, url): client = AsyncHTTPClient() client.fetch(url, self._handle_response) def _handle_response(self, response): # The underscore will make it private automatically # You could also use @private if you wished # This returns the status code of the request self.result(response.code) Debugging --------- There is a `config` object that is available -- it will be expanded as time goes by. Currently, it supports two options: `verbose` and `short_errors`, both of which default to True. The `verbose` setting just specifies whether you want to print out results to the terminal (automatically on, you'll probably want to turn that off for production, WSGI deployment, etc.) and the `short_errors` option determines whether to print just the last few lines of the traceback (if set to True, default) or print the full traceback. Once the logging mechanism is in place, the `short_errors` configuration element will apply to that as well. The default error look something similar to this: JSON-RPC SERVER AT http://localhost:8484 --------------- ERROR IN messup --------------- Traceback (most recent call last): File "test.py", line 20, in messup return doesntexist['bad_key'] NameError: global name 'doesntexist' is not defined To change the configuration, look over the following: import tornadorpc tornadorpc.config.verbose = False tornadorpc.config.short_errors = False # or... from tornadorpc import config config.verbose = False config.short_errors = False Tests ----- To run some basic tests, enter the following in the same directory that this README is in: python run_tests.py This will test a few basic utilites and the XMLRPC system. If you wish to test the JSONRPC system, run the following: python run_tests.py --json TODO ---- * Add unit tests * Add logging mechanism * Add proper HTTP codes for failures * Optimize Platform: UNKNOWN tornadorpc-0.1.1/tornadorpc.egg-info/requires.txt0000644000076500000240000000002212310673650023226 0ustar jmarshallstaff00000000000000tornado jsonrpclibtornadorpc-0.1.1/tornadorpc.egg-info/SOURCES.txt0000644000076500000240000000043312310673650022520 0ustar jmarshallstaff00000000000000README setup.py tornadorpc/__init__.py tornadorpc/base.py tornadorpc/json.py tornadorpc/utils.py tornadorpc/xml.py tornadorpc.egg-info/PKG-INFO tornadorpc.egg-info/SOURCES.txt tornadorpc.egg-info/dependency_links.txt tornadorpc.egg-info/requires.txt tornadorpc.egg-info/top_level.txttornadorpc-0.1.1/tornadorpc.egg-info/top_level.txt0000644000076500000240000000001312310673650023360 0ustar jmarshallstaff00000000000000tornadorpc