├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.markdown ├── TODO.markdown ├── bertrpc ├── __init__.py ├── client.py └── error.py ├── setup.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | *.egg-info 4 | MANIFEST 5 | *.pyc 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Michael J. Russo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.markdown 2 | include LICENSE 3 | include MANIFEST.in 4 | include tests.py 5 | 6 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | BERTRPC 2 | ======= 3 | 4 | A BERT-RPC client library for Python. A port of Tom Preston-Werner's [Ruby library](http://github.com/mojombo/bertrpc). 5 | 6 | See the full BERT-RPC specification at [bert-rpc.org](http://bert-rpc.org). 7 | 8 | This library currently only supports the following BERT-RPC features: 9 | 10 | * `call` requests 11 | * `cast` requests 12 | 13 | Installation 14 | ------------ 15 | 16 | Install from PyPI: 17 | 18 | easy_install bertrpc 19 | 20 | Examples 21 | -------- 22 | 23 | Import the library and create an RPC client: 24 | 25 | import bertrpc 26 | service = bertrpc.Service('localhost', 9999) 27 | 28 | ### Make a call: 29 | 30 | response = service.request('call').calc.add(1, 2) 31 | 32 | Note that the underlying BERT-RPC transaction of the above call is: 33 | 34 | -> {call, calc, add, [1, 2]} 35 | <- {reply, 3} 36 | 37 | In this example, the value of the `response` variable is `3`. 38 | 39 | ### Make a cast: 40 | 41 | service.request('cast').stats.incr() 42 | 43 | Note that the underlying BERT-RPC transaction of the above cast is: 44 | 45 | -> {cast, stats, incr, []} 46 | <- {noreply} 47 | 48 | The value of the `response` variable is `None` for all successful cast calls. 49 | 50 | Running the unit tests 51 | ---------------------- 52 | 53 | To run the unit tests, execute the following command from the root of the project directory: 54 | 55 | python tests.py 56 | 57 | Copyright 58 | --------- 59 | 60 | Copyright (c) 2009 Michael J. Russo. See LICENSE for details. -------------------------------------------------------------------------------- /TODO.markdown: -------------------------------------------------------------------------------- 1 | BERTRPC TODO 2 | ============ 3 | 4 | Action 5 | ------ 6 | 7 | - refactor/clean up \_transaction method of Action class 8 | 9 | Testing 10 | ------- 11 | 12 | - Unit tests for Decoder class 13 | - Unit tests for Action class 14 | 15 | Info 16 | ---- 17 | 18 | - add support for reading 'info' responses sent from the server 19 | -------------------------------------------------------------------------------- /bertrpc/__init__.py: -------------------------------------------------------------------------------- 1 | """BERT-RPC Client Library""" 2 | 3 | from client import Service 4 | -------------------------------------------------------------------------------- /bertrpc/client.py: -------------------------------------------------------------------------------- 1 | import bert 2 | import error 3 | import socket 4 | import struct 5 | 6 | 7 | class Service(object): 8 | def __init__(self, host, port, timeout = None): 9 | self.host = host 10 | self.port = port 11 | self.timeout = timeout 12 | 13 | def request(self, kind, options=None): 14 | if kind in ['call', 'cast']: 15 | self._verify_options(options) 16 | return Request(self, bert.Atom(kind), options) 17 | else: 18 | raise error.InvalidRequest('unsupported request of kind: "%s"' % kind) 19 | 20 | def _verify_options(self, options): 21 | if options is not None: 22 | cache = options.get('cache', None) 23 | if cache is not None: 24 | if len(cache) >= 2 and cache[0] == 'validation' and type(cache[1]) == type(str()): 25 | pass 26 | else: 27 | raise error.InvalidOption('Valid cache args are [validation, String]') 28 | else: 29 | raise error.InvalidOption('Valid options are: cache') 30 | 31 | 32 | class Request(object): 33 | def __init__(self, service, kind, options): 34 | self.service = service 35 | self.kind = kind 36 | self.options = options 37 | 38 | def __getattr__(self, attr): 39 | return Module(self.service, self, bert.Atom(attr)) 40 | 41 | 42 | class Module(object): 43 | def __init__(self, service, request, module): 44 | self.service = service 45 | self.request = request 46 | self.module = module 47 | 48 | def __getattr__(self, attr): 49 | def callable(*args, **kwargs): 50 | return self.method_missing(attr, *args, **kwargs) 51 | return callable 52 | 53 | def method_missing(self, *args, **kwargs): 54 | return Action(self.service, 55 | self.request, 56 | self.module, 57 | bert.Atom(args[0]), 58 | list(args[1:])).execute() 59 | 60 | 61 | class Action(object): 62 | def __init__(self, service, request, module, function, arguments): 63 | self.service = service 64 | self.request = request 65 | self.module = module 66 | self.function = function 67 | self.arguments = arguments 68 | 69 | def execute(self): 70 | python_request = (self.request.kind, 71 | self.module, 72 | self.function, 73 | self.arguments) 74 | bert_request = Encoder().encode(python_request) 75 | bert_response = self._transaction(bert_request) 76 | python_response = Decoder().decode(bert_response) 77 | return python_response 78 | 79 | def _transaction(self, bert_request): 80 | try: 81 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 82 | sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 83 | if self.service.timeout is not None: sock.settimeout(self.service.timeout) 84 | sock.connect((self.service.host, self.service.port)) 85 | if self.request.options is not None: 86 | if self.request.options.get('cache', None) is not None: 87 | if self.request.options['cache'][0] == 'validation': 88 | token = self.request.options['cache'][1] 89 | info_bert = Encoder().encode( 90 | (bert.Atom('info'), bert.Atom('cache'), [bert.Atom('validation'), bert.Atom(token)])) 91 | info_header = struct.pack(">l", len(info_bert)) 92 | sock.sendall(info_header) 93 | sock.sendall(info_bert) 94 | header = struct.pack(">l", len(bert_request)) 95 | sock.sendall(header) 96 | sock.sendall(bert_request) 97 | lenheader = sock.recv(4) 98 | if lenheader is None: raise error.ProtocolError(error.ProtocolError.NO_HEADER) 99 | length = struct.unpack(">l",lenheader)[0] 100 | 101 | bert_response = '' 102 | while len(bert_response) < length: 103 | response_part = sock.recv(length - len(bert_response)) 104 | if response_part is None or len(response_part) == 0: 105 | raise error.ProtocolError(error.ProtocolError.NO_DATA) 106 | bert_response += response_part 107 | 108 | sock.close() 109 | return bert_response 110 | except socket.timeout, e: 111 | raise error.ReadTimeoutError('No response from %s:%s in %ss' % 112 | (self.service.host, self.service.port, self.service.timeout)) 113 | except socket.error, e: 114 | raise error.ConnectionError('Unable to connect to %s:%s' % (self.service.host, self.service.port)) 115 | 116 | 117 | class Encoder(object): 118 | def encode(self, python_request): 119 | return bert.encode(python_request) 120 | 121 | 122 | class Decoder(object): 123 | def decode(self, bert_response): 124 | python_response = bert.decode(bert_response) 125 | if python_response[0] == bert.Atom('reply'): 126 | return python_response[1] 127 | elif python_response[0] == bert.Atom('noreply'): 128 | return None 129 | elif python_response[0] == bert.Atom('error'): 130 | return self._error(python_response[1]) 131 | else: 132 | raise error.BERTRPCError('invalid response received from server') 133 | 134 | def _error(self, err): 135 | level, code, klass, message, backtrace = err 136 | exception_map = { 137 | bert.Atom('protocol'): error.ProtocolError, 138 | bert.Atom('server'): error.ServerError, 139 | bert.Atom('user'): error.UserError, 140 | bert.Atom('proxy'): error.ProxyError 141 | } 142 | exception = exception_map.get(level, None) 143 | if level is not None: 144 | raise exception([code, message], klass, backtrace) 145 | else: 146 | raise error.BERTRPCError('invalid error code received from server') 147 | 148 | 149 | if __name__ == '__main__': 150 | print 'initializing service now' 151 | service = Service('localhost', 9999) 152 | 153 | print 'RPC call now' 154 | response = service.request('call').calc.add(1, 2) 155 | print 'response is: %s' % repr(response) 156 | 157 | print 'RPC call now, with options' 158 | options = {'cache': ['validation','myToken']} 159 | response = service.request('call', options).calc.add(5, 6) 160 | print 'response is: %s' % repr(response) 161 | 162 | print 'RPC cast now' 163 | response = service.request('cast').stats.incr() 164 | print 'response is: %s' % repr(response) 165 | -------------------------------------------------------------------------------- /bertrpc/error.py: -------------------------------------------------------------------------------- 1 | class BERTRPCError(Exception): 2 | def __init__(self, msg = None, klass = None, bt = []): 3 | Exception.__init__(self, msg) 4 | if type(msg) == type(list()): 5 | code, message = msg[0], msg[1:] 6 | else: 7 | code, message = [0, msg] 8 | self.code = code 9 | self.message = message 10 | self.klass = klass 11 | self.bt = bt 12 | 13 | def __str__(self): 14 | details = [] 15 | if self.bt is not None and len(self.bt) > 0: 16 | details.append('Traceback:\n%s\n' % ('\n'.join(self.bt))) 17 | if self.klass is not None: 18 | details.append('Class: %s\n' % self.klass) 19 | if self.code is not None: 20 | details.append('Code: %s\n' % self.code) 21 | details.append('%s: %s' % (self.__class__.__name__, self.message)) 22 | return ''.join(details) 23 | 24 | # override the python 2.6 DeprecationWarning re: 'message' property 25 | def _get_message(self): return self._message 26 | def _set_message(self, message): self._message = message 27 | message = property(_get_message, _set_message) 28 | 29 | 30 | class RemoteError(BERTRPCError): 31 | pass 32 | 33 | 34 | class ConnectionError(BERTRPCError): 35 | pass 36 | 37 | 38 | class ReadTimeoutError(BERTRPCError): 39 | pass 40 | 41 | 42 | class ProtocolError(BERTRPCError): 43 | NO_HEADER = [0, "Unable to read length header from server."] 44 | NO_DATA = [1, "Unable to read data from server."] 45 | 46 | 47 | class ServerError(BERTRPCError): 48 | pass 49 | 50 | 51 | class UserError(BERTRPCError): 52 | pass 53 | 54 | 55 | class ProxyError(BERTRPCError): 56 | pass 57 | 58 | 59 | class InvalidRequest(BERTRPCError): 60 | pass 61 | 62 | 63 | class InvalidOption(BERTRPCError): 64 | pass 65 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | __version__ = '0.1.1' 4 | 5 | setup( 6 | name = 'bertrpc', 7 | license='MIT', 8 | version = __version__, 9 | description = 'BERT-RPC Library', 10 | author = 'Michael J. Russo', 11 | author_email = 'mjrusso@gmail.com', 12 | url = 'http://github.com/mjrusso/python-bertrpc', 13 | packages = ['bertrpc'], 14 | install_requires = ["erlastic", "bert"], 15 | classifiers = [ 16 | 'Intended Audience :: Developers', 17 | 'Operating System :: OS Independent', 18 | 'Programming Language :: Python', 19 | 'Topic :: Software Development :: Libraries :: Python Modules', 20 | ], 21 | ) 22 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import bert 2 | import bertrpc 3 | import unittest 4 | 5 | 6 | class TestService(unittest.TestCase): 7 | def testValidRequestInitializationNoTimeout(self): 8 | service = bertrpc.Service('localhost', 9999) 9 | service_with_timeout = bertrpc.Service('localhost', 9999, 12) 10 | self.assertEqual('localhost', service.host) 11 | self.assertEqual(9999, service.port) 12 | self.assertEqual(None, service.timeout) 13 | 14 | def testValidRequestInitializationWithTimeout(self): 15 | service = bertrpc.Service('localhost', 9999, 12) 16 | self.assertEqual('localhost', service.host) 17 | self.assertEqual(9999, service.port) 18 | self.assertEqual(12, service.timeout) 19 | 20 | def testInvalidRequestKind(self): 21 | service = bertrpc.Service('localhost', 9999) 22 | request_kind = 'jump' # valid options are 'call', 'cast', ... 23 | self.assertRaises(bertrpc.error.InvalidRequest, service.request, request_kind) 24 | 25 | def testValidRequestOptions(self): 26 | service = bertrpc.Service('localhost', 9999) 27 | options = { 28 | 'cache': [ 29 | 'validation', 30 | 'myToken' 31 | ] 32 | } 33 | request = service.request('call',options) 34 | self.assertEqual(options, request.options) 35 | 36 | def testInvalidRequestOptions(self): 37 | service = bertrpc.Service('localhost', 9999) 38 | options1 = { 39 | 'fakeOption': 0 40 | } 41 | options2 = { 42 | 'cache': [ 43 | 'validation', 44 | 1234 45 | ] 46 | } 47 | self.assertRaises(bertrpc.error.InvalidOption, service.request, 'call', options1) 48 | self.assertRaises(bertrpc.error.InvalidOption, service.request, 'call', options2) 49 | 50 | 51 | class TestEncodes(unittest.TestCase): 52 | def testRequestEncoder(self): 53 | bert_encoded = "\203h\004d\000\004calld\000\005mymodd\000\005myfunl\000\000\000\003a\001a\002a\003j" 54 | request = (bert.Atom('call'), bert.Atom('mymod'), bert.Atom('myfun'), [1, 2, 3]) 55 | self.assertEqual(bert_encoded, bertrpc.client.Encoder().encode(request)) 56 | 57 | 58 | if __name__ == '__main__': 59 | unittest.main() 60 | --------------------------------------------------------------------------------