Permalink
Browse files

Initial import.

git-svn-id: http://tornadorpc.googlecode.com/svn/trunk@2 6a0cc88c-bc38-11de-9a73-11f8458f6c97
  • Loading branch information...
1 parent d3fc8cf commit d653d7639b0dfd11876630ba7f77adc52eaf0b46 catchjosh committed Oct 19, 2009
Showing with 586 additions and 0 deletions.
  1. +29 −0 setup.py
  2. +16 −0 tornadorpc/__init__.py
  3. +316 −0 tornadorpc/base.py
  4. +123 −0 tornadorpc/json.py
  5. +102 −0 tornadorpc/xml.py
View
@@ -0,0 +1,29 @@
+#!/usr/bin/env/python
+"""
+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.
+"""
+
+import distutils.core
+
+distutils.core.setup(
+ name = "tornadorpc",
+ version = "0.1",
+ packages = ["tornadorpc"],
+ author = "Josh Marshall",
+ 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.",
+)
View
@@ -0,0 +1,16 @@
+"""
+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, start_server
View
@@ -0,0 +1,316 @@
+"""
+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.
+
+============================
+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 types
+
+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
+
+ @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
+ requests = self.parse_request(request_body)
+ if type(requests) is not types.TupleType:
+ # SHOULD be the result of a fault call,
+ # according tothe parse_request spec below.
+ return requests
+ responses = []
+ for request in requests:
+ response = self.dispatch(request[0], request[1])
+ responses.append(response)
+ responses = tuple(responses)
+ 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)
+ return response_text
+
+ 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 method_name in dir(RequestHandler):
+ # Pre-existing, not an implemented attribute
+ return 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.faults.method_not_found()
+ if not callable(method):
+ # Not callable, so not a method
+ return self.faults.method_not_found()
+ if method_name.startswith('_') or \
+ ('private' in dir(method) and method.private is True):
+ # No, no. That's private.
+ return self.faults.method_not_found()
+ if type(params) is types.DictType:
+ # Keyword arguments
+ try:
+ response = method(**params)
+ except TypeError:
+ return self.faults.invalid_params()
+ except:
+ # We should log here...bare excepts are evil.
+ return self.faults.internal_error()
+ return response
+ elif type(params) in (types.ListType, types.TupleType):
+ # Positional arguments
+ try:
+ response = method(*params)
+ except TypeError:
+ return self.faults.invalid_params()
+ except:
+ # Once again, we need to log here
+ return self.faults.internal_error()
+ return response
+ else:
+ # Bad argument formatting?
+ return self.faults.invalid_params()
+
+ 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 'private' in dir(attr) and attr.private == True:
+ 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. The only attribute
+ this adds to the Tornado request handler is '_RPC_', which
+ is what holds the RPC Parser subclassed from the
+ BaseRPCParser above.
+ """
+ _RPC_ = None
+
+ def post(self):
+ # Very simple -- dispatches request body to the parser
+ # and returns the output
+ request_body = self.request.body
+ response_text = self._RPC_.run(self, request_body)
+ self.set_header('Content-Type', self._RPC_.content_type)
+ self.write(response_text)
+ return
+
+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(obj):
+ """
+ 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.
+ """
+ class PrivateMethod(object):
+ def __init__(self):
+ self.private = True
+ __call__ = obj
+ return PrivateMethod()
+
+def start_server(handler, 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)
+ """
+ import tornado.web
+ import tornado.ioloop
+ import tornado.httpserver
+
+ routes = [(route, handler),]
+ if not route.endswith('/'):
+ route = r'%s/' % route
+ routes.append(('%sRPC2' % route, handler))
+
+ application = tornado.web.Application(routes)
+ http_server = tornado.httpserver.HTTPServer(application)
+ http_server.listen(port)
+ tornado.ioloop.IOLoop.instance().start()
+
+
+"""
+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
+
+ tree = TestMethodTree()
+
+ def _private(self):
+ # Shouldn't be called
+ return False
+
+ @private
+ def private(self):
+ # Also shouldn't be called
+ return False
Oops, something went wrong.

0 comments on commit d653d76

Please sign in to comment.