Skip to content
Browse files

initial cut of bertrpc

  • Loading branch information...
1 parent 24d9b71 commit 505df88a2f4a94db771db6af6c966b17c4bdbedb @mjrusso committed Dec 29, 2009
Showing with 402 additions and 1 deletion.
  1. +77 −1 README.markdown
  2. +18 −0 TODO.markdown
  3. +7 −0 bertrpc/__init__.py
  4. +158 −0 bertrpc/client.py
  5. +64 −0 bertrpc/error.py
  6. +19 −0 setup.py
  7. +59 −0 tests.py
View
78 README.markdown
@@ -3,7 +3,83 @@ BERTRPC
By Michael J. Russo (mjrusso at gmail dot com)
-A BERT-RPC client library for Python.
+A BERT-RPC client library for Python. A port of Tom Preston-Werner's [Ruby library](http://github.com/mojombo/bertrpc).
+
+See the full BERT-RPC specification at [bert-rpc.org](http://bert-rpc.org).
+
+This library currently only supports the following BERT-RPC features:
+
+* `call` requests
+* `cast` requests
+
+Installation
+------------
+
+* Install the dependencies:
+
+ * erlastic
+
+ $> git clone git://github.com/samuel/python-erlastic.git
+ $> cd python-erlastic
+ $> python setup.py install
+
+ * bert
+
+ $> git clone git://github.com/samuel/python-bert.git
+ $> cd python-bert
+ $> python setup.py install
+
+* Install this library:
+
+ $> git clone git://github.com/mjrusso/python-bertrpc.git
+ $> cd python-bertrpc
+ $> python setup.py install
+
+
+Examples
+--------
+
+---
+
+Import the library and create an RPC client:
+
+ import bertrpc
+ service = bertrpc.Service('localhost', 9999)
+
+---
+
+Make a call:
+
+ response = service.request('call').calc.add(1, 2)
+
+Note that the underlying BERT-RPC transaction of the above call is:
+
+ -> {call, calc, add, [1, 2]}
+ <- {reply, 3}
+
+In this example, the value of the `response` variable is `3`.
+
+---
+
+Make a cast:
+
+ service.request('cast').stats.incr()
+
+Note that the underlying BERT-RPC transaction of the above cast is:
+
+ -> {cast, stats, incr, []}
+ <- {noreply}
+
+The value of the `response` variable is `None` for all successful cast calls.
+
+---
+
+Running the unit tests
+----------------------
+
+To run the unit tests, execute the following command from the root of the project directory:
+
+ python tests.py
Copyright
---------
View
18 TODO.markdown
@@ -0,0 +1,18 @@
+BERTRPC TODO
+============
+
+Action
+------
+
+- refactor/clean up \_transaction method of Action class
+
+Testing
+-------
+
+- Unit tests for Decoder class
+- Unit tests for Action class
+
+Info
+----
+
+- add support for reading 'info' responses sent from the server
View
7 bertrpc/__init__.py
@@ -0,0 +1,7 @@
+"""BERT-RPC Client Library"""
+
+__version__ = '0.0.1'
+
+from client import Service
+
+
View
158 bertrpc/client.py
@@ -0,0 +1,158 @@
+import bert
+import error
+import socket
+import struct
+
+
+class Service(object):
+ def __init__(self, host, port, timeout = None):
+ self.host = host
+ self.port = port
+ self.timeout = timeout
+
+ def request(self, kind, options=None):
+ if kind in ['call', 'cast']:
+ self._verify_options(options)
+ return Request(self, bert.Atom(kind), options)
+ else:
+ raise error.InvalidRequest('unsupported request of kind: "%s"' % kind)
+
+ def _verify_options(self, options):
+ if options is not None:
+ cache = options.get('cache', None)
+ if cache is not None:
+ if len(cache) >= 2 and cache[0] == 'validation' and type(cache[1]) == type(str()):
+ pass
+ else:
+ raise error.InvalidOption('Valid cache args are [validation, String]')
+ else:
+ raise error.InvalidOption('Valid options are: cache')
+
+
+class Request(object):
+ def __init__(self, service, kind, options):
+ self.service = service
+ self.kind = kind
+ self.options = options
+
+ def __getattr__(self, attr):
+ return Module(self.service, self, bert.Atom(attr))
+
+
+class Module(object):
+ def __init__(self, service, request, module):
+ self.service = service
+ self.request = request
+ self.module = module
+
+ def __getattr__(self, attr):
+ def callable(*args, **kwargs):
+ return self.method_missing(attr, *args, **kwargs)
+ return callable
+
+ def method_missing(self, *args, **kwargs):
+ return Action(self.service,
+ self.request,
+ self.module,
+ bert.Atom(args[0]),
+ list(args[1:])).execute()
+
+
+class Action(object):
+ def __init__(self, service, request, module, function, arguments):
+ self.service = service
+ self.request = request
+ self.module = module
+ self.function = function
+ self.arguments = arguments
+
+ def execute(self):
+ python_request = (self.request.kind,
+ self.module,
+ self.function,
+ self.arguments)
+ bert_request = Encoder().encode(python_request)
+ bert_response = self._transaction(bert_request)
+ python_response = Decoder().decode(bert_response)
+ return python_response
+
+ def _transaction(self, bert_request):
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ if self.service.timeout is not None: sock.settimeout(self.service.timeout)
+ sock.connect((self.service.host, self.service.port))
+ if self.request.options is not None:
+ if self.request.options.get('cache', None) is not None:
+ if self.request.options['cache'][0] == 'validation':
+ token = self.request.options['cache'][1]
+ info_bert = Encoder().encode(
+ (bert.Atom('info'), bert.Atom('cache'), [bert.Atom('validation'), bert.Atom(token)]))
+ info_header = struct.pack(">l", len(info_bert))
+ sock.sendall(info_header)
+ sock.sendall(info_bert)
+ header = struct.pack(">l", len(bert_request))
+ sock.sendall(header)
+ sock.sendall(bert_request)
+ lenheader = sock.recv(4)
+ if lenheader is None: raise error.ProtocolError(error.ProtocolError.NO_HEADER)
+ length = struct.unpack(">l",lenheader)[0]
+ bert_response = sock.recv(length)
+ if bert_response is None or len(bert_response) == 0: raise error.ProtocolError(error.ProtocolError.NO_DATA)
+ sock.close()
+ return bert_response
+ except socket.timeout, e:
+ raise error.ReadTimeoutError('No response from %s:%s in %ss' %
+ (self.service.host, self.service.port, self.service.timeout))
+ except socket.error, e:
+ raise error.ConnectionError('Unable to connect to %s:%s' % (self.service.host, self.service.port))
+
+
+class Encoder(object):
+ def encode(self, python_request):
+ return bert.encode(python_request)
+
+
+class Decoder(object):
+ def decode(self, bert_response):
+ python_response = bert.decode(bert_response)
+ if python_response[0] == bert.Atom('reply'):
+ return python_response[1]
+ elif python_response[0] == bert.Atom('noreply'):
+ return None
+ elif python_response[0] == bert.Atom('error'):
+ return self._error(python_response[1])
+ else:
+ raise error.BERTRPCError('invalid response received from server')
+
+ def _error(self, err):
+ level, code, klass, message, backtrace = err
+ exception_map = {
+ bert.Atom('protocol'): error.ProtocolError,
+ bert.Atom('server'): error.ServerError,
+ bert.Atom('user'): error.UserError,
+ bert.Atom('proxy'): error.ProxyError
+ }
+ exception = exception_map.get(level, None)
+ if level is not None:
+ raise exception([code, message], klass, backtrace)
+ else:
+ raise error.BERTRPCError('invalid error code received from server')
+
+
+if __name__ == '__main__':
+ print 'initializing service now'
+ service = Service('localhost', 9999)
+
+ print 'RPC call now'
+ response = service.request('call').calc.add(1, 2)
+ print 'response is: %s' % repr(response)
+
+ print 'RPC call now, with options'
+ options = {'cache': ['validation','myToken']}
+ response = service.request('call', options).calc.add(5, 6)
+ print 'response is: %s' % repr(response)
+
+ print 'RPC cast now'
+ response = service.request('cast').stats.incr()
+ print 'response is: %s' % repr(response)
View
64 bertrpc/error.py
@@ -0,0 +1,64 @@
+class BERTRPCError(Exception):
+ def __init__(self, msg = None, klass = None, bt = []):
+ Exception.__init__(self, msg)
+ if type(msg) == type(list()):
+ code, message = msg[0], msg[1:]
+ else:
+ code, message = [0, msg]
+ self.code = code
+ self.message = message
+ self.klass = klass
+ self.bt = bt
+
+ def __str__(self):
+ details = []
+ if self.bt is not None and len(self.bt) > 0:
+ details.append('Traceback:\n%s\n' % ('\n'.join(self.bt)))
+ if self.klass is not None:
+ details.append('Class: %s\n' % self.klass)
+ if self.code is not None:
+ details.append('Code: %s\n' % self.code)
+ details.append('%s: %s' % (self.__class__.__name__, self.message))
+ return ''.join(details)
+
+ # override the python 2.6 DeprecationWarning re: 'message' property
+ def _get_message(self): return self._message
+ def _set_message(self, message): self._message = message
+ message = property(_get_message, _set_message)
+
+
+class RemoteError(BERTRPCError):
+ pass
+
+
+class ConnectionError(BERTRPCError):
+ pass
+
+
+class ReadTimeoutError(BERTRPCError):
+ pass
+
+
+class ProtocolError(BERTRPCError):
+ NO_HEADER = [0, "Unable to read length header from server."]
+ NO_DATA = [1, "Unable to read data from server."]
+
+
+class ServerError(BERTRPCError):
+ pass
+
+
+class UserError(BERTRPCError):
+ pass
+
+
+class ProxyError(BERTRPCError):
+ pass
+
+
+class InvalidRequest(BERTRPCError):
+ pass
+
+
+class InvalidOption(BERTRPCError):
+ pass
View
19 setup.py
@@ -0,0 +1,19 @@
+from distutils.core import setup
+
+from bert import __version__ as version
+
+setup(
+ name = 'bertrpc',
+ version = version,
+ description = 'BERT-RPC Library',
+ author = 'Michael J. Russo',
+ author_email = 'mjrusso@gmail.com',
+ url = 'http://github.com/mjrusso/python-bertrpc',
+ packages = ['bertrpc'],
+ classifiers = [
+ 'Intended Audience :: Developers',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ ],
+)
View
59 tests.py
@@ -0,0 +1,59 @@
+import bert
+import bertrpc
+import unittest
+
+
+class TestService(unittest.TestCase):
+ def testValidRequestInitializationNoTimeout(self):
+ service = bertrpc.Service('localhost', 9999)
+ service_with_timeout = bertrpc.Service('localhost', 9999, 12)
+ self.assertEqual('localhost', service.host)
+ self.assertEqual(9999, service.port)
+ self.assertEqual(None, service.timeout)
+
+ def testValidRequestInitializationWithTimeout(self):
+ service = bertrpc.Service('localhost', 9999, 12)
+ self.assertEqual('localhost', service.host)
+ self.assertEqual(9999, service.port)
+ self.assertEqual(12, service.timeout)
+
+ def testInvalidRequestKind(self):
+ service = bertrpc.Service('localhost', 9999)
+ request_kind = 'jump' # valid options are 'call', 'cast', ...
+ self.assertRaises(bertrpc.error.InvalidRequest, service.request, request_kind)
+
+ def testValidRequestOptions(self):
+ service = bertrpc.Service('localhost', 9999)
+ options = {
+ 'cache': [
+ 'validation',
+ 'myToken'
+ ]
+ }
+ request = service.request('call',options)
+ self.assertEqual(options, request.options)
+
+ def testInvalidRequestOptions(self):
+ service = bertrpc.Service('localhost', 9999)
+ options1 = {
+ 'fakeOption': 0
+ }
+ options2 = {
+ 'cache': [
+ 'validation',
+ 1234
+ ]
+ }
+ self.assertRaises(bertrpc.error.InvalidOption, service.request, 'call', options1)
+ self.assertRaises(bertrpc.error.InvalidOption, service.request, 'call', options2)
+
+
+class TestEncodes(unittest.TestCase):
+ def testRequestEncoder(self):
+ bert_encoded = "\203h\004d\000\004calld\000\005mymodd\000\005myfunl\000\000\000\003a\001a\002a\003j"
+ request = (bert.Atom('call'), bert.Atom('mymod'), bert.Atom('myfun'), [1, 2, 3])
+ self.assertEqual(bert_encoded, bertrpc.client.Encoder().encode(request))
+
+
+if __name__ == '__main__':
+ unittest.main()

0 comments on commit 505df88

Please sign in to comment.
Something went wrong with that request. Please try again.