PageRenderTime 7ms CodeModel.GetById 5ms app.highlight 60ms RepoModel.GetById 1ms app.codeStats 0ms

/tests/manual/python/jsonrpc.py

https://bitbucket.org/devonit/qjsonrpc
Python | 1075 lines | 1054 code | 11 blank | 10 comment | 10 complexity | 3fb343ea93e3a60cd2ffcf9084970fc5 MD5 | raw file
   1#!/usr/bin/env python
   2# -*- coding: ascii -*-
   3"""
   4JSON-RPC (remote procedure call).
   5
   6It consists of 3 (independent) parts:
   7    - proxy/dispatcher
   8    - data structure / serializer
   9    - transport
  10
  11It's intended for JSON-RPC, but since the above 3 parts are independent,
  12it could be used for other RPCs as well.
  13
  14Currently, JSON-RPC 2.0(pre) and JSON-RPC 1.0 are implemented
  15
  16:Version:   2008-08-31-beta
  17:Status:    experimental
  18
  19:Example:
  20    simple Client with JsonRPC2.0 and TCP/IP::
  21
  22        >>> proxy = ServerProxy( JsonRpc20(), TransportTcpIp(addr=("127.0.0.1",31415)) )
  23        >>> proxy.echo( "hello world" )
  24        u'hello world'
  25        >>> proxy.echo( "bye." )
  26        u'bye.'
  27
  28    simple Server with JsonRPC2.0 and TCP/IP with logging to STDOUT::
  29
  30        >>> server = Server( JsonRpc20(), TransportTcpIp(addr=("127.0.0.1",31415), logfunc=log_stdout) )
  31        >>> def echo( s ):
  32        ...   return s
  33        >>> server.register_function( echo )
  34        >>> server.serve( 2 )   # serve 2 requests          # doctest: +ELLIPSIS
  35        listen ('127.0.0.1', 31415)
  36        ('127.0.0.1', ...) connected
  37        ('127.0.0.1', ...) <-- {"jsonrpc": "2.0", "method": "echo", "params": ["hello world"], "id": 0}
  38        ('127.0.0.1', ...) --> {"jsonrpc": "2.0", "result": "hello world", "id": 0}
  39        ('127.0.0.1', ...) close
  40        ('127.0.0.1', ...) connected
  41        ('127.0.0.1', ...) <-- {"jsonrpc": "2.0", "method": "echo", "params": ["bye."], "id": 0}
  42        ('127.0.0.1', ...) --> {"jsonrpc": "2.0", "result": "bye.", "id": 0}
  43        ('127.0.0.1', ...) close
  44        close ('127.0.0.1', 31415)
  45
  46    Client with JsonRPC2.0 and an abstract Unix Domain Socket::
  47    
  48        >>> proxy = ServerProxy( JsonRpc20(), TransportUnixSocket(addr="\\x00.rpcsocket") )
  49        >>> proxy.hi( message="hello" )         #named parameters
  50        u'hi there'
  51        >>> proxy.test()                        #fault
  52        Traceback (most recent call last):
  53          ...
  54        jsonrpc.RPCMethodNotFound: <RPCFault -32601: u'Method not found.' (None)>
  55        >>> proxy.debug.echo( "hello world" )   #hierarchical procedures
  56        u'hello world'
  57
  58    Server with JsonRPC2.0 and abstract Unix Domain Socket with a logfile::
  59        
  60        >>> server = Server( JsonRpc20(), TransportUnixSocket(addr="\\x00.rpcsocket", logfunc=log_file("mylog.txt")) )
  61        >>> def echo( s ):
  62        ...   return s
  63        >>> def hi( message ):
  64        ...   return "hi there"
  65        >>> server.register_function( hi )
  66        >>> server.register_function( echo, name="debug.echo" )
  67        >>> server.serve( 3 )   # serve 3 requests
  68
  69        "mylog.txt" then contains:
  70        listen '\\x00.rpcsocket'
  71        '' connected
  72        '' --> '{"jsonrpc": "2.0", "method": "hi", "params": {"message": "hello"}, "id": 0}'
  73        '' <-- '{"jsonrpc": "2.0", "result": "hi there", "id": 0}'
  74        '' close
  75        '' connected
  76        '' --> '{"jsonrpc": "2.0", "method": "test", "id": 0}'
  77        '' <-- '{"jsonrpc": "2.0", "error": {"code":-32601, "message": "Method not found."}, "id": 0}'
  78        '' close
  79        '' connected
  80        '' --> '{"jsonrpc": "2.0", "method": "debug.echo", "params": ["hello world"], "id": 0}'
  81        '' <-- '{"jsonrpc": "2.0", "result": "hello world", "id": 0}'
  82        '' close
  83        close '\\x00.rpcsocket'
  84
  85:Note:      all exceptions derived from RPCFault are propagated to the client.
  86            other exceptions are logged and result in a sent-back "empty" INTERNAL_ERROR.
  87:Uses:      simplejson, socket, sys,time,codecs
  88:SeeAlso:   JSON-RPC 2.0 proposal, 1.0 specification
  89:Warning:
  90    .. Warning::
  91        This is **experimental** code!
  92:Bug:
  93
  94:Author:    Roland Koebler (rk(at)simple-is-better.org)
  95:Copyright: 2007-2008 by Roland Koebler (rk(at)simple-is-better.org)
  96:License:   see __license__
  97:Changelog:
  98        - 2008-08-31:     1st release
  99
 100TODO:
 101        - server: multithreading rpc-server
 102        - client: multicall (send several requests)
 103        - transport: SSL sockets, maybe HTTP, HTTPS
 104        - types: support for date/time (ISO 8601)
 105        - errors: maybe customizable error-codes/exceptions
 106        - mixed 1.0/2.0 server ?
 107        - system description etc. ?
 108        - maybe test other json-serializers, like cjson?
 109"""
 110
 111__version__ = "2008-08-31-beta"
 112__author__   = "Roland Koebler <rk(at)simple-is-better.org>"
 113__license__  = """Copyright (c) 2007-2008 by Roland Koebler (rk(at)simple-is-better.org)
 114
 115Permission is hereby granted, free of charge, to any person obtaining
 116a copy of this software and associated documentation files (the
 117"Software"), to deal in the Software without restriction, including
 118without limitation the rights to use, copy, modify, merge, publish,
 119distribute, sublicense, and/or sell copies of the Software, and to
 120permit persons to whom the Software is furnished to do so, subject to
 121the following conditions:
 122
 123The above copyright notice and this permission notice shall be included
 124in all copies or substantial portions of the Software.
 125
 126THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 127EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 128MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 129IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 130CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 131TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 132SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."""
 133
 134#=========================================
 135#import
 136
 137import sys
 138
 139#=========================================
 140# errors
 141
 142#----------------------
 143# error-codes + exceptions
 144
 145#JSON-RPC 2.0 error-codes
 146PARSE_ERROR           = -32700
 147INVALID_REQUEST       = -32600
 148METHOD_NOT_FOUND      = -32601
 149INVALID_METHOD_PARAMS = -32602  #invalid number/type of parameters
 150INTERNAL_ERROR        = -32603  #"all other errors"
 151
 152#additional error-codes
 153PROCEDURE_EXCEPTION    = -32000
 154AUTHENTIFICATION_ERROR = -32001
 155PERMISSION_DENIED      = -32002
 156INVALID_PARAM_VALUES   = -32003
 157
 158#human-readable messages
 159ERROR_MESSAGE = {
 160    PARSE_ERROR           : "Parse error.",
 161    INVALID_REQUEST       : "Invalid Request.",
 162    METHOD_NOT_FOUND      : "Method not found.",
 163    INVALID_METHOD_PARAMS : "Invalid parameters.",
 164    INTERNAL_ERROR        : "Internal error.",
 165
 166    PROCEDURE_EXCEPTION   : "Procedure exception.",
 167    AUTHENTIFICATION_ERROR : "Authentification error.",
 168    PERMISSION_DENIED   : "Permission denied.",
 169    INVALID_PARAM_VALUES: "Invalid parameter values."
 170    }
 171 
 172#----------------------
 173# exceptions
 174
 175class RPCError(Exception):
 176    """Base class for rpc-errors."""
 177
 178
 179class RPCTransportError(RPCError):
 180    """Transport error."""
 181class RPCTimeoutError(RPCTransportError):
 182    """Transport/reply timeout."""
 183
 184class RPCFault(RPCError):
 185    """RPC error/fault package received.
 186    
 187    This exception can also be used as a class, to generate a
 188    RPC-error/fault message.
 189
 190    :Variables:
 191        - error_code:   the RPC error-code
 192        - error_string: description of the error
 193        - error_data:   optional additional information
 194                        (must be json-serializable)
 195    :TODO: improve __str__
 196    """
 197    def __init__(self, error_code, error_message, error_data=None):
 198        RPCError.__init__(self)
 199        self.error_code   = error_code
 200        self.error_message = error_message
 201        self.error_data   = error_data
 202    def __str__(self):
 203        return repr(self)
 204    def __repr__(self):
 205        return( "<RPCFault %s: %s (%s)>" % (self.error_code, repr(self.error_message), repr(self.error_data)) )
 206
 207class RPCParseError(RPCFault):
 208    """Broken rpc-package. (PARSE_ERROR)"""
 209    def __init__(self, error_data=None):
 210        RPCFault.__init__(self, PARSE_ERROR, ERROR_MESSAGE[PARSE_ERROR], error_data)
 211
 212class RPCInvalidRPC(RPCFault):
 213    """Invalid rpc-package. (INVALID_REQUEST)"""
 214    def __init__(self, error_data=None):
 215        RPCFault.__init__(self, INVALID_REQUEST, ERROR_MESSAGE[INVALID_REQUEST], error_data)
 216
 217class RPCMethodNotFound(RPCFault):
 218    """Method not found. (METHOD_NOT_FOUND)"""
 219    def __init__(self, error_data=None):
 220        RPCFault.__init__(self, METHOD_NOT_FOUND, ERROR_MESSAGE[METHOD_NOT_FOUND], error_data)
 221
 222class RPCInvalidMethodParams(RPCFault):
 223    """Invalid method-parameters. (INVALID_METHOD_PARAMS)"""
 224    def __init__(self, error_data=None):
 225        RPCFault.__init__(self, INVALID_METHOD_PARAMS, ERROR_MESSAGE[INVALID_METHOD_PARAMS], error_data)
 226
 227class RPCInternalError(RPCFault):
 228    """Internal error. (INTERNAL_ERROR)"""
 229    def __init__(self, error_data=None):
 230        RPCFault.__init__(self, INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], error_data)
 231
 232
 233class RPCProcedureException(RPCFault):
 234    """Procedure exception. (PROCEDURE_EXCEPTION)"""
 235    def __init__(self, error_data=None):
 236        RPCFault.__init__(self, PROCEDURE_EXCEPTION, ERROR_MESSAGE[PROCEDURE_EXCEPTION], error_data)
 237class RPCAuthentificationError(RPCFault):
 238    """AUTHENTIFICATION_ERROR"""
 239    def __init__(self, error_data=None):
 240        RPCFault.__init__(self, AUTHENTIFICATION_ERROR, ERROR_MESSAGE[AUTHENTIFICATION_ERROR], error_data)
 241class RPCPermissionDenied(RPCFault):
 242    """PERMISSION_DENIED"""
 243    def __init__(self, error_data=None):
 244        RPCFault.__init__(self, PERMISSION_DENIED, ERROR_MESSAGE[PERMISSION_DENIED], error_data)
 245class RPCInvalidParamValues(RPCFault):
 246    """INVALID_PARAM_VALUES"""
 247    def __init__(self, error_data=None):
 248        RPCFault.__init__(self, INVALID_PARAM_VALUES, ERROR_MESSAGE[INVALID_PARAM_VALUES], error_data)
 249
 250
 251#=========================================
 252# data structure / serializer
 253
 254try:
 255    import simplejson
 256except ImportError, err:
 257    print "FATAL: json-module 'simplejson' is missing (%s)" % (err)
 258    sys.exit(1)
 259
 260#----------------------
 261#
 262def dictkeyclean(d):
 263    """Convert all keys of the dict 'd' to (ascii-)strings.
 264
 265    :Raises: UnicodeEncodeError
 266    """
 267    new_d = {}
 268    for (k, v) in d.iteritems():
 269        new_d[str(k)] = v
 270    return new_d
 271
 272#----------------------
 273# JSON-RPC 1.0
 274
 275class JsonRpc10:
 276    """JSON-RPC V1.0 data-structure / serializer
 277
 278    This implementation is quite liberal in what it accepts: It treats
 279    missing "params" and "id" in Requests and missing "result"/"error" in
 280    Responses as empty/null.
 281
 282    :SeeAlso:   JSON-RPC 1.0 specification
 283    :TODO:      catch simplejson.dumps not-serializable-exceptions
 284    """
 285    def __init__(self, dumps=simplejson.dumps, loads=simplejson.loads):
 286        """init: set serializer to use
 287
 288        :Parameters:
 289            - dumps: json-encoder-function
 290            - loads: json-decoder-function
 291        :Note: The dumps_* functions of this class already directly create
 292               the invariant parts of the resulting json-object themselves,
 293               without using the given json-encoder-function.
 294        """
 295        self.dumps = dumps
 296        self.loads = loads
 297
 298    def dumps_request( self, method, params=(), id=0 ):
 299        """serialize JSON-RPC-Request
 300
 301        :Parameters:
 302            - method: the method-name (str/unicode)
 303            - params: the parameters (list/tuple)
 304            - id:     if id=None, this results in a Notification
 305        :Returns:   | {"method": "...", "params": ..., "id": ...}
 306                    | "method", "params" and "id" are always in this order.
 307        :Raises:    TypeError if method/params is of wrong type or 
 308                    not JSON-serializable
 309        """
 310        if not isinstance(method, (str, unicode)):
 311            raise TypeError('"method" must be a string (or unicode string).')
 312        if not isinstance(params, (tuple, list)):
 313            raise TypeError("params must be a tuple/list.")
 314
 315        return '{"method": %s, "params": %s, "id": %s}' % \
 316                (self.dumps(method), self.dumps(params), self.dumps(id))
 317
 318    def dumps_notification( self, method, params=() ):
 319        """serialize a JSON-RPC-Notification
 320
 321        :Parameters: see dumps_request
 322        :Returns:   | {"method": "...", "params": ..., "id": null}
 323                    | "method", "params" and "id" are always in this order.
 324        :Raises:    see dumps_request
 325        """
 326        if not isinstance(method, (str, unicode)):
 327            raise TypeError('"method" must be a string (or unicode string).')
 328        if not isinstance(params, (tuple, list)):
 329            raise TypeError("params must be a tuple/list.")
 330
 331        return '{"method": %s, "params": %s, "id": null}' % \
 332                (self.dumps(method), self.dumps(params))
 333
 334    def dumps_response( self, result, id=None ):
 335        """serialize a JSON-RPC-Response (without error)
 336
 337        :Returns:   | {"result": ..., "error": null, "id": ...}
 338                    | "result", "error" and "id" are always in this order.
 339        :Raises:    TypeError if not JSON-serializable
 340        """
 341        return '{"result": %s, "error": null, "id": %s}' % \
 342                (self.dumps(result), self.dumps(id))
 343
 344    def dumps_error( self, error, id=None ):
 345        """serialize a JSON-RPC-Response-error
 346
 347        Since JSON-RPC 1.0 does not define an error-object, this uses the
 348        JSON-RPC 2.0 error-object.
 349      
 350        :Parameters:
 351            - error: a RPCFault instance
 352        :Returns:   | {"result": null, "error": {"code": error_code, "message": error_message, "data": error_data}, "id": ...}
 353                    | "result", "error" and "id" are always in this order, data is omitted if None.
 354        :Raises:    ValueError if error is not a RPCFault instance,
 355                    TypeError if not JSON-serializable
 356        """
 357        if not isinstance(error, RPCFault):
 358            raise ValueError("""error must be a RPCFault-instance.""")
 359        if error.error_data is None:
 360            return '{"result": null, "error": {"code":%s, "message": %s}, "id": %s}' % \
 361                    (self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(id))
 362        else:
 363            return '{"result": null, "error": {"code":%s, "message": %s, "data": %s}, "id": %s}' % \
 364                    (self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(error.error_data), self.dumps(id))
 365
 366    def loads_request( self, string ):
 367        """de-serialize a JSON-RPC Request/Notification
 368
 369        :Returns:   | [method_name, params, id] or [method_name, params]
 370                    | params is a tuple/list
 371                    | if id is missing, this is a Notification
 372        :Raises:    RPCParseError, RPCInvalidRPC, RPCInvalidMethodParams
 373        """
 374        try:
 375            data = self.loads(string)
 376        except ValueError, err:
 377            raise RPCParseError("No valid JSON. (%s)" % str(err))
 378        if not isinstance(data, dict):  raise RPCInvalidRPC("No valid RPC-package.")
 379        if "method" not in data:        raise RPCInvalidRPC("""Invalid Request, "method" is missing.""")
 380        if not isinstance(data["method"], (str, unicode)):
 381            raise RPCInvalidRPC("""Invalid Request, "method" must be a string.""")
 382        if "id"     not in data:        data["id"]     = None   #be liberal
 383        if "params" not in data:        data["params"] = ()     #be liberal
 384        if not isinstance(data["params"], (list, tuple)):
 385            raise RPCInvalidRPC("""Invalid Request, "params" must be an array.""")
 386        if len(data) != 3:          raise RPCInvalidRPC("""Invalid Request, additional fields found.""")
 387
 388        # notification / request
 389        if data["id"] is None:
 390            return data["method"], data["params"]               #notification
 391        else:
 392            return data["method"], data["params"], data["id"]   #request
 393
 394    def loads_response( self, string ):
 395        """de-serialize a JSON-RPC Response/error
 396
 397        :Returns: | [result, id] for Responses
 398        :Raises:  | RPCFault+derivates for error-packages/faults, RPCParseError, RPCInvalidRPC
 399                  | Note that for error-packages which do not match the
 400                    V2.0-definition, RPCFault(-1, "Error", RECEIVED_ERROR_OBJ)
 401                    is raised.
 402        """
 403        try:
 404            data = self.loads(string)
 405        except ValueError, err:
 406            raise RPCParseError("No valid JSON. (%s)" % str(err))
 407        if not isinstance(data, dict):  raise RPCInvalidRPC("No valid RPC-package.")
 408        if "id" not in data:            raise RPCInvalidRPC("""Invalid Response, "id" missing.""")
 409        if "result" not in data:        data["result"] = None    #be liberal
 410        if "error"  not in data:        data["error"]  = None    #be liberal
 411        if len(data) != 3:              raise RPCInvalidRPC("""Invalid Response, additional or missing fields.""")
 412
 413        #error
 414        if data["error"] is not None:
 415            if data["result"] is not None:
 416                raise RPCInvalidRPC("""Invalid Response, one of "result" or "error" must be null.""")
 417            #v2.0 error-format
 418            if( isinstance(data["error"], dict)  and  "code" in data["error"]  and  "message" in data["error"]  and
 419                (len(data["error"])==2 or ("data" in data["error"] and len(data["error"])==3)) ):
 420                if "data" not in data["error"]:
 421                    error_data = None
 422                else:
 423                    error_data = data["error"]["data"]
 424
 425                if   data["error"]["code"] == PARSE_ERROR:
 426                    raise RPCParseError(error_data)
 427                elif data["error"]["code"] == INVALID_REQUEST:
 428                    raise RPCInvalidRPC(error_data)
 429                elif data["error"]["code"] == METHOD_NOT_FOUND:
 430                    raise RPCMethodNotFound(error_data)
 431                elif data["error"]["code"] == INVALID_METHOD_PARAMS:
 432                    raise RPCInvalidMethodParams(error_data)
 433                elif data["error"]["code"] == INTERNAL_ERROR:
 434                    raise RPCInternalError(error_data)
 435                elif data["error"]["code"] == PROCEDURE_EXCEPTION:
 436                    raise RPCProcedureException(error_data)
 437                elif data["error"]["code"] == AUTHENTIFICATION_ERROR:
 438                    raise RPCAuthentificationError(error_data)
 439                elif data["error"]["code"] == PERMISSION_DENIED:
 440                    raise RPCPermissionDenied(error_data)
 441                elif data["error"]["code"] == INVALID_PARAM_VALUES:
 442                    raise RPCInvalidParamValues(error_data)
 443                else:
 444                    raise RPCFault(data["error"]["code"], data["error"]["message"], error_data)
 445            #other error-format
 446            else:
 447                raise RPCFault(-1, "Error", data["error"])
 448        #result
 449        else:
 450            return data["result"], data["id"]
 451
 452#----------------------
 453# JSON-RPC 2.0
 454
 455class JsonRpc20:
 456    """JSON-RPC V2.0 data-structure / serializer
 457
 458    :SeeAlso:   JSON-RPC 2.0 specification
 459    :TODO:      catch simplejson.dumps not-serializable-exceptions
 460    """
 461    def __init__(self, dumps=simplejson.dumps, loads=simplejson.loads):
 462        """init: set serializer to use
 463
 464        :Parameters:
 465            - dumps: json-encoder-function
 466            - loads: json-decoder-function
 467        :Note: The dumps_* functions of this class already directly create
 468               the invariant parts of the resulting json-object themselves,
 469               without using the given json-encoder-function.
 470        """
 471        self.dumps = dumps
 472        self.loads = loads
 473
 474    def dumps_request( self, method, params=(), id=0 ):
 475        """serialize JSON-RPC-Request
 476
 477        :Parameters:
 478            - method: the method-name (str/unicode)
 479            - params: the parameters (list/tuple/dict)
 480            - id:     the id (should not be None)
 481        :Returns:   | {"jsonrpc": "2.0", "method": "...", "params": ..., "id": ...}
 482                    | "jsonrpc", "method", "params" and "id" are always in this order.
 483                    | "params" is omitted if empty
 484        :Raises:    TypeError if method/params is of wrong type or 
 485                    not JSON-serializable
 486        """
 487        if not isinstance(method, (str, unicode)):
 488            raise TypeError('"method" must be a string (or unicode string).')
 489        if not isinstance(params, (tuple, list, dict)):
 490            raise TypeError("params must be a tuple/list/dict or None.")
 491
 492        if params:
 493            return '{"jsonrpc": "2.0", "method": %s, "params": %s, "id": %s}' % \
 494                    (self.dumps(method), self.dumps(params), self.dumps(id))
 495        else:
 496            return '{"jsonrpc": "2.0", "method": %s, "id": %s}' % \
 497                    (self.dumps(method), self.dumps(id))
 498
 499    def dumps_notification( self, method, params=() ):
 500        """serialize a JSON-RPC-Notification
 501
 502        :Parameters: see dumps_request
 503        :Returns:   | {"jsonrpc": "2.0", "method": "...", "params": ...}
 504                    | "jsonrpc", "method" and "params" are always in this order.
 505        :Raises:    see dumps_request
 506        """
 507        if not isinstance(method, (str, unicode)):
 508            raise TypeError('"method" must be a string (or unicode string).')
 509        if not isinstance(params, (tuple, list, dict)):
 510            raise TypeError("params must be a tuple/list/dict or None.")
 511
 512        if params:
 513            return '{"jsonrpc": "2.0", "method": %s, "params": %s}' % \
 514                    (self.dumps(method), self.dumps(params))
 515        else:
 516            return '{"jsonrpc": "2.0", "method": %s}' % \
 517                    (self.dumps(method))
 518
 519    def dumps_response( self, result, id=None ):
 520        """serialize a JSON-RPC-Response (without error)
 521
 522        :Returns:   | {"jsonrpc": "2.0", "result": ..., "id": ...}
 523                    | "jsonrpc", "result", and "id" are always in this order.
 524        :Raises:    TypeError if not JSON-serializable
 525        """
 526        return '{"jsonrpc": "2.0", "result": %s, "id": %s}' % \
 527                (self.dumps(result), self.dumps(id))
 528
 529    def dumps_error( self, error, id=None ):
 530        """serialize a JSON-RPC-Response-error
 531      
 532        :Parameters:
 533            - error: a RPCFault instance
 534        :Returns:   | {"jsonrpc": "2.0", "error": {"code": error_code, "message": error_message, "data": error_data}, "id": ...}
 535                    | "jsonrpc", "result", "error" and "id" are always in this order, data is omitted if None.
 536        :Raises:    ValueError if error is not a RPCFault instance,
 537                    TypeError if not JSON-serializable
 538        """
 539        if not isinstance(error, RPCFault):
 540            raise ValueError("""error must be a RPCFault-instance.""")
 541        if error.error_data is None:
 542            return '{"jsonrpc": "2.0", "error": {"code":%s, "message": %s}, "id": %s}' % \
 543                    (self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(id))
 544        else:
 545            return '{"jsonrpc": "2.0", "error": {"code":%s, "message": %s, "data": %s}, "id": %s}' % \
 546                    (self.dumps(error.error_code), self.dumps(error.error_message), self.dumps(error.error_data), self.dumps(id))
 547
 548    def loads_request( self, string ):
 549        """de-serialize a JSON-RPC Request/Notification
 550
 551        :Returns:   | [method_name, params, id] or [method_name, params]
 552                    | params is a tuple/list or dict (with only str-keys)
 553                    | if id is missing, this is a Notification
 554        :Raises:    RPCParseError, RPCInvalidRPC, RPCInvalidMethodParams
 555        """
 556        try:
 557            data = self.loads(string)
 558        except ValueError, err:
 559            raise RPCParseError("No valid JSON. (%s)" % str(err))
 560        if not isinstance(data, dict):  raise RPCInvalidRPC("No valid RPC-package.")
 561        if "jsonrpc" not in data:       raise RPCInvalidRPC("""Invalid Response, "jsonrpc" missing.""")
 562        if not isinstance(data["jsonrpc"], (str, unicode)):
 563            raise RPCInvalidRPC("""Invalid Response, "jsonrpc" must be a string.""")
 564        if data["jsonrpc"] != "2.0":    raise RPCInvalidRPC("""Invalid jsonrpc version.""")
 565        if "method" not in data:        raise RPCInvalidRPC("""Invalid Request, "method" is missing.""")
 566        if not isinstance(data["method"], (str, unicode)):
 567            raise RPCInvalidRPC("""Invalid Request, "method" must be a string.""")
 568        if "params" not in data:        data["params"] = ()
 569        #convert params-keys from unicode to str
 570        elif isinstance(data["params"], dict):
 571            try:
 572                data["params"] = dictkeyclean(data["params"])
 573            except UnicodeEncodeError:
 574                raise RPCInvalidMethodParams("Parameter-names must be in ascii.")
 575        elif not isinstance(data["params"], (list, tuple)):
 576            raise RPCInvalidRPC("""Invalid Request, "params" must be an array or object.""")
 577        if not( len(data)==3 or ("id" in data and len(data)==4) ):
 578            raise RPCInvalidRPC("""Invalid Request, additional fields found.""")
 579
 580        # notification / request
 581        if "id" not in data:
 582            return data["method"], data["params"]               #notification
 583        else:
 584            return data["method"], data["params"], data["id"]   #request
 585
 586    def loads_response( self, string ):
 587        """de-serialize a JSON-RPC Response/error
 588
 589        :Returns: | [result, id] for Responses
 590        :Raises:  | RPCFault+derivates for error-packages/faults, RPCParseError, RPCInvalidRPC
 591        """
 592        try:
 593            data = self.loads(string)
 594        except ValueError, err:
 595            raise RPCParseError("No valid JSON. (%s)" % str(err))
 596        if not isinstance(data, dict):  raise RPCInvalidRPC("No valid RPC-package.")
 597        if "jsonrpc" not in data:       raise RPCInvalidRPC("""Invalid Response, "jsonrpc" missing.""")
 598        if not isinstance(data["jsonrpc"], (str, unicode)):
 599            raise RPCInvalidRPC("""Invalid Response, "jsonrpc" must be a string.""")
 600        if data["jsonrpc"] != "2.0":    raise RPCInvalidRPC("""Invalid jsonrpc version.""")
 601        if "id" not in data:            raise RPCInvalidRPC("""Invalid Response, "id" missing.""")
 602        if "result" not in data:        data["result"] = None
 603        if "error"  not in data:        data["error"]  = None
 604        if len(data) != 4:              raise RPCInvalidRPC("""Invalid Response, additional or missing fields.""")
 605
 606        #error
 607        if data["error"] is not None:
 608            if data["result"] is not None:
 609                raise RPCInvalidRPC("""Invalid Response, only "result" OR "error" allowed.""")
 610            if not isinstance(data["error"], dict): raise RPCInvalidRPC("Invalid Response, invalid error-object.")
 611            if "code" not in data["error"]  or  "message" not in data["error"]:
 612                raise RPCInvalidRPC("Invalid Response, invalid error-object.")
 613            if "data" not in data["error"]:  data["error"]["data"] = None
 614            if len(data["error"]) != 3:
 615                raise RPCInvalidRPC("Invalid Response, invalid error-object.")
 616
 617            error_data = data["error"]["data"]
 618            if   data["error"]["code"] == PARSE_ERROR:
 619                raise RPCParseError(error_data)
 620            elif data["error"]["code"] == INVALID_REQUEST:
 621                raise RPCInvalidRPC(error_data)
 622            elif data["error"]["code"] == METHOD_NOT_FOUND:
 623                raise RPCMethodNotFound(error_data)
 624            elif data["error"]["code"] == INVALID_METHOD_PARAMS:
 625                raise RPCInvalidMethodParams(error_data)
 626            elif data["error"]["code"] == INTERNAL_ERROR:
 627                raise RPCInternalError(error_data)
 628            elif data["error"]["code"] == PROCEDURE_EXCEPTION:
 629                raise RPCProcedureException(error_data)
 630            elif data["error"]["code"] == AUTHENTIFICATION_ERROR:
 631                raise RPCAuthentificationError(error_data)
 632            elif data["error"]["code"] == PERMISSION_DENIED:
 633                raise RPCPermissionDenied(error_data)
 634            elif data["error"]["code"] == INVALID_PARAM_VALUES:
 635                raise RPCInvalidParamValues(error_data)
 636            else:
 637                raise RPCFault(data["error"]["code"], data["error"]["message"], error_data)
 638        #result
 639        else:
 640            return data["result"], data["id"]
 641
 642
 643#=========================================
 644# transports
 645
 646#----------------------
 647# transport-logging
 648
 649import codecs
 650import time
 651
 652def log_dummy( message ):
 653    """dummy-logger: do nothing"""
 654    pass
 655def log_stdout( message ):
 656    """print message to STDOUT"""
 657    print message
 658
 659def log_file( filename ):
 660    """return a logfunc which logs to a file (in utf-8)"""
 661    def logfile( message ):
 662        f = codecs.open( filename, 'a', encoding='utf-8' )
 663        f.write( message+"\n" )
 664        f.close()
 665    return logfile
 666
 667def log_filedate( filename ):
 668    """return a logfunc which logs date+message to a file (in utf-8)"""
 669    def logfile( message ):
 670        f = codecs.open( filename, 'a', encoding='utf-8' )
 671        f.write( time.strftime("%Y-%m-%d %H:%M:%S ")+message+"\n" )
 672        f.close()
 673    return logfile
 674
 675#----------------------
 676
 677class Transport:
 678    """generic Transport-interface.
 679    
 680    This class, and especially its methods and docstrings,
 681    define the Transport-Interface.
 682    """
 683    def __init__(self):
 684        pass
 685
 686    def send( self, data ):
 687        """send all data. must be implemented by derived classes."""
 688        raise NotImplementedError
 689    def recv( self ):
 690        """receive data. must be implemented by derived classes."""
 691        raise NotImplementedError
 692
 693    def sendrecv( self, string ):
 694        """send + receive data"""
 695        self.send( string )
 696        return self.recv()
 697    def serve( self, handler, n=None ):
 698        """serve (forever or for n communicaions).
 699        
 700        - receive data
 701        - call result = handler(data)
 702        - send back result if not None
 703
 704        The serving can be stopped by SIGINT.
 705
 706        :TODO:
 707            - how to stop?
 708              maybe use a .run-file, and stop server if file removed?
 709            - maybe make n_current accessible? (e.g. for logging)
 710        """
 711        n_current = 0
 712        while 1:
 713            if n is not None  and  n_current >= n:
 714                break
 715            data = self.recv()
 716            result = handler(data)
 717            if result is not None:
 718                self.send( result )
 719            n_current += 1
 720
 721
 722class TransportSTDINOUT(Transport):
 723    """receive from STDIN, send to STDOUT.
 724
 725    Useful e.g. for debugging.
 726    """
 727    def send(self, string):
 728        """write data to STDOUT with '***SEND:' prefix """
 729        print "***SEND:"
 730        print string
 731    def recv(self):
 732        """read data from STDIN"""
 733        print "***RECV (please enter, ^D ends.):"
 734        return sys.stdin.read()
 735
 736
 737import socket, select
 738class TransportSocket(Transport):
 739    """Transport via socket.
 740   
 741    :SeeAlso:   python-module socket
 742    :TODO:
 743        - documentation
 744        - improve this (e.g. make sure that connections are closed, socket-files are deleted etc.)
 745        - exception-handling? (socket.error)
 746    """
 747    def __init__( self, addr, limit=4096, sock_type=socket.AF_INET, sock_prot=socket.SOCK_STREAM, timeout=1.0, logfunc=log_dummy ):
 748        """
 749        :Parameters:
 750            - addr: socket-address
 751            - timeout: timeout in seconds
 752            - logfunc: function for logging, logfunc(message)
 753        :Raises: socket.timeout after timeout
 754        """
 755        self.limit  = limit
 756        self.addr   = addr
 757        self.s_type = sock_type
 758        self.s_prot = sock_prot
 759        self.s      = None
 760        self.timeout = timeout
 761        self.log    = logfunc
 762    def connect( self ):
 763        self.close()
 764        self.log( "connect to %s" % repr(self.addr) )
 765        self.s = socket.socket( self.s_type, self.s_prot )
 766        self.s.settimeout( self.timeout )
 767        self.s.connect( self.addr )
 768    def close( self ):
 769        if self.s is not None:
 770            self.log( "close %s" % repr(self.addr) )
 771            self.s.close()
 772            self.s = None
 773    def __repr__(self):
 774        return "<TransportSocket, %s>" % repr(self.addr)
 775    
 776    def send( self, string ):
 777        if self.s is None:
 778            self.connect()
 779        self.log( "--> "+repr(string) )
 780        self.s.sendall( string )
 781    def recv( self ):
 782        if self.s is None:
 783            self.connect()
 784        data = self.s.recv( self.limit )
 785        while( select.select((self.s,), (), (), 0.1)[0] ):  #TODO: this select is probably not necessary, because server closes this socket
 786            d = self.s.recv( self.limit )
 787            if len(d) == 0:
 788                break
 789            data += d
 790        self.log( "<-- "+repr(data) )
 791        return data
 792
 793    def sendrecv( self, string ):
 794        """send data + receive data + close"""
 795        try:
 796            self.send( string )
 797            return self.recv()
 798        finally:
 799            self.close()
 800    def serve(self, handler, n=None):
 801        """open socket, wait for incoming connections and handle them.
 802        
 803        :Parameters:
 804            - n: serve n requests, None=forever
 805        """
 806        self.close()
 807        self.s = socket.socket( self.s_type, self.s_prot )
 808        try:
 809            self.log( "listen %s" % repr(self.addr) )
 810            self.s.bind( self.addr )
 811            self.s.listen(1)
 812            n_current = 0
 813            while 1:
 814                if n is not None  and  n_current >= n:
 815                    break
 816                conn, addr = self.s.accept()
 817                self.log( "%s connected" % repr(addr) )
 818                data = conn.recv(self.limit)
 819                self.log( "%s --> %s" % (repr(addr), repr(data)) )
 820                result = handler(data)
 821                if data is not None:
 822                    self.log( "%s <-- %s" % (repr(addr), repr(result)) )
 823                    conn.send( result )
 824                self.log( "%s close" % repr(addr) )
 825                conn.close()
 826                n_current += 1
 827        finally:
 828            self.close()
 829
 830
 831if hasattr(socket, 'AF_UNIX'):
 832    
 833    class TransportUnixSocket(TransportSocket):
 834        """Transport via Unix Domain Socket.
 835        """
 836        def __init__(self, addr=None, limit=4096, timeout=1.0, logfunc=log_dummy):
 837            """
 838            :Parameters:
 839                - addr: "socket_file"
 840            :Note: | The socket-file is not deleted.
 841                   | If the socket-file begins with \x00, abstract sockets are used,
 842                     and no socket-file is created.
 843            :SeeAlso:   TransportSocket
 844            """
 845            TransportSocket.__init__( self, addr, limit, socket.AF_UNIX, socket.SOCK_STREAM, timeout, logfunc )
 846
 847class TransportTcpIp(TransportSocket):
 848    """Transport via TCP/IP.
 849    """
 850    def __init__(self, addr=None, limit=4096, timeout=1.0, logfunc=log_dummy):
 851        """
 852        :Parameters:
 853            - addr: ("host",port)
 854        :SeeAlso:   TransportSocket
 855        """
 856        TransportSocket.__init__( self, addr, limit, socket.AF_INET, socket.SOCK_STREAM, timeout, logfunc )
 857
 858
 859#=========================================
 860# client side: server proxy
 861
 862class ServerProxy:
 863    """RPC-client: server proxy
 864
 865    A logical connection to a RPC server.
 866
 867    It works with different data/serializers and different transports.
 868
 869    Notifications and id-handling/multicall are not yet implemented.
 870
 871    :Example:
 872        see module-docstring
 873
 874    :TODO: verbose/logging?
 875    """
 876    def __init__( self, data_serializer, transport ):
 877        """
 878        :Parameters:
 879            - data_serializer: a data_structure+serializer-instance
 880            - transport: a Transport instance
 881        """
 882        #TODO: check parameters
 883        self.__data_serializer = data_serializer
 884        if not isinstance(transport, Transport):
 885            raise ValueError('invalid "transport" (must be a Transport-instance)"')
 886        self.__transport = transport
 887
 888    def __str__(self):
 889        return repr(self)
 890    def __repr__(self):
 891        return "<ServerProxy for %s, with serializer %s>" % (self.__transport, self.__data_serializer)
 892
 893    def __req( self, methodname, args=None, kwargs=None, id=0 ):
 894        # JSON-RPC 1.0: only positional parameters
 895        if len(kwargs) > 0 and isinstance(self.data_serializer, JsonRpc10):
 896            raise ValueError("Only positional parameters allowed in JSON-RPC 1.0")
 897        # JSON-RPC 2.0: only args OR kwargs allowed!
 898        if len(args) > 0 and len(kwargs) > 0:
 899            raise ValueError("Only positional or named parameters are allowed!")
 900        if len(kwargs) == 0:
 901            req_str  = self.__data_serializer.dumps_request( methodname, args, id )
 902        else:
 903            req_str  = self.__data_serializer.dumps_request( methodname, kwargs, id )
 904
 905        try:
 906            resp_str = self.__transport.sendrecv( req_str )
 907        except Exception,err:
 908            raise RPCTransportError(err)
 909        resp = self.__data_serializer.loads_response( resp_str )
 910        return resp[0]
 911
 912    def __getattr__(self, name):
 913        # magic method dispatcher
 914        #  note: to call a remote object with an non-standard name, use
 915        #  result getattr(my_server_proxy, "strange-python-name")(args)
 916        return _method(self.__req, name)
 917
 918# request dispatcher
 919class _method:
 920    """some "magic" to bind an RPC method to an RPC server.
 921
 922    Supports "nested" methods (e.g. examples.getStateName).
 923
 924    :Raises: AttributeError for method-names/attributes beginning with '_'.
 925    """
 926    def __init__(self, req, name):
 927        if name[0] == "_":  #prevent rpc-calls for proxy._*-functions
 928            raise AttributeError("invalid attribute '%s'" % name)
 929        self.__req  = req
 930        self.__name = name
 931    def __getattr__(self, name):
 932        if name[0] == "_":  #prevent rpc-calls for proxy._*-functions
 933            raise AttributeError("invalid attribute '%s'" % name)
 934        return _method(self.__req, "%s.%s" % (self.__name, name))
 935    def __call__(self, *args, **kwargs):
 936        return self.__req(self.__name, args, kwargs)
 937
 938#=========================================
 939# server side: Server
 940
 941class Server:
 942    """RPC-server.
 943
 944    It works with different data/serializers and 
 945    with different transports.
 946
 947    :Example:
 948        see module-docstring
 949
 950    :TODO:
 951        - mixed JSON-RPC 1.0/2.0 server?
 952        - logging/loglevels?
 953    """
 954    def __init__( self, data_serializer, transport, logfile=None ):
 955        """
 956        :Parameters:
 957            - data_serializer: a data_structure+serializer-instance
 958            - transport: a Transport instance
 959            - logfile: file to log ("unexpected") errors to
 960        """
 961        #TODO: check parameters
 962        self.__data_serializer = data_serializer
 963        if not isinstance(transport, Transport):
 964            raise ValueError('invalid "transport" (must be a Transport-instance)"')
 965        self.__transport = transport
 966        self.logfile = logfile
 967        if self.logfile is not None:    #create logfile (or raise exception)
 968            f = codecs.open( self.logfile, 'a', encoding='utf-8' )
 969            f.close()
 970
 971        self.funcs = {}
 972
 973    def __repr__(self):
 974        return "<Server for %s, with serializer %s>" % (self.__transport, self.__data_serializer)
 975
 976    def log(self, message):
 977        """write a message to the logfile (in utf-8)"""
 978        if self.logfile is not None:
 979            f = codecs.open( self.logfile, 'a', encoding='utf-8' )
 980            f.write( time.strftime("%Y-%m-%d %H:%M:%S ")+message+"\n" )
 981            f.close()
 982
 983    def register_instance(self, myinst, name=None):
 984        """Add all functions of a class-instance to the RPC-services.
 985        
 986        All entries of the instance which do not begin with '_' are added.
 987
 988        :Parameters:
 989            - myinst: class-instance containing the functions
 990            - name:   | hierarchical prefix.
 991                      | If omitted, the functions are added directly.
 992                      | If given, the functions are added as "name.function".
 993        :TODO:
 994            - only add functions and omit attributes?
 995            - improve hierarchy?
 996        """
 997        for e in dir(myinst):
 998            if e[0][0] != "_":
 999                if name is None:
1000                    self.register_function( getattr(myinst, e) )
1001                else:
1002                    self.register_function( getattr(myinst, e), name="%s.%s" % (name, e) )
1003    def register_function(self, function, name=None):
1004        """Add a function to the RPC-services.
1005        
1006        :Parameters:
1007            - function: function to add
1008            - name:     RPC-name for the function. If omitted/None, the original
1009                        name of the function is used.
1010        """
1011        if name is None:
1012            self.funcs[function.__name__] = function
1013        else:
1014            self.funcs[name] = function
1015    
1016    def handle(self, rpcstr):
1017        """Handle a RPC-Request.
1018
1019        :Parameters:
1020            - rpcstr: the received rpc-string
1021        :Returns: the data to send back or None if nothing should be sent back
1022        :Raises:  RPCFault (and maybe others)
1023        """
1024        #TODO: id
1025        notification = False
1026        try:
1027            req = self.__data_serializer.loads_request( rpcstr )
1028            if len(req) == 2:       #notification
1029                method, params = req
1030                notification = True
1031            else:                   #request
1032                method, params, id = req
1033        except RPCFault, err:
1034            return self.__data_serializer.dumps_error( err, id=None )
1035        except Exception, err:
1036            self.log( "%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err)) )
1037            return self.__data_serializer.dumps_error( RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id=None )
1038
1039        if method not in self.funcs:
1040            if notification:
1041                return None
1042            return self.__data_serializer.dumps_error( RPCFault(METHOD_NOT_FOUND, ERROR_MESSAGE[METHOD_NOT_FOUND]), id )
1043
1044        try:
1045            if isinstance(params, dict):
1046                result = self.funcs[method]( **params )
1047            else:
1048                result = self.funcs[method]( *params )
1049        except RPCFault, err:
1050            if notification:
1051                return None
1052            return self.__data_serializer.dumps_error( err, id=None )
1053        except Exception, err:
1054            if notification:
1055                return None
1056            self.log( "%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err)) )
1057            return self.__data_serializer.dumps_error( RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id )
1058
1059        if notification:
1060            return None
1061        try:
1062            return self.__data_serializer.dumps_response( result, id )
1063        except Exception, err:
1064            self.log( "%d (%s): %s" % (INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR], str(err)) )
1065            return self.__data_serializer.dumps_error( RPCFault(INTERNAL_ERROR, ERROR_MESSAGE[INTERNAL_ERROR]), id )
1066
1067    def serve(self, n=None):
1068        """serve (forever or for n communicaions).
1069        
1070        :See: Transport
1071        """
1072        self.__transport.serve( self.handle, n )
1073
1074#=========================================
1075