1 | # -*- coding: utf-8 -*- |
---|
2 | # This program is free software; you can redistribute it and/or modify |
---|
3 | # it under the terms of the GNU Lesser General Public License as published by the |
---|
4 | # Free Software Foundation; either version 3, or (at your option) any later |
---|
5 | # version. |
---|
6 | # |
---|
7 | # This program is distributed in the hope that it will be useful, but |
---|
8 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY |
---|
9 | # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
---|
10 | # for more details. |
---|
11 | |
---|
12 | "Pythonic simple JSON RPC Client implementation" |
---|
13 | from __future__ import print_function |
---|
14 | |
---|
15 | __author__ = "Mariano Reingart (reingart@gmail.com)" |
---|
16 | __copyright__ = "Copyright (C) 2011 Mariano Reingart" |
---|
17 | __license__ = "LGPL 3.0" |
---|
18 | __version__ = "0.05" |
---|
19 | |
---|
20 | import sys |
---|
21 | PY2 = sys.version_info[0] == 2 |
---|
22 | |
---|
23 | if PY2: |
---|
24 | import urllib |
---|
25 | from xmlrpclib import Transport, SafeTransport |
---|
26 | from cStringIO import StringIO |
---|
27 | else: |
---|
28 | import urllib.request as urllib |
---|
29 | from xmlrpc.client import Transport, SafeTransport |
---|
30 | from io import StringIO |
---|
31 | import random |
---|
32 | import json |
---|
33 | from gluon._compat import basestring |
---|
34 | |
---|
35 | class JSONRPCError(RuntimeError): |
---|
36 | "Error object for remote procedure call fail" |
---|
37 | def __init__(self, code, message, data=''): |
---|
38 | if isinstance(data, basestring): |
---|
39 | data = [data] |
---|
40 | value = "%s: %s\n%s" % (code, message, '\n'.join(data)) |
---|
41 | RuntimeError.__init__(self, value) |
---|
42 | self.code = code |
---|
43 | self.message = message |
---|
44 | self.data = data |
---|
45 | |
---|
46 | |
---|
47 | class JSONDummyParser: |
---|
48 | "json wrapper for xmlrpclib parser interfase" |
---|
49 | def __init__(self): |
---|
50 | self.buf = StringIO() |
---|
51 | |
---|
52 | def feed(self, data): |
---|
53 | self.buf.write(data.decode('utf-8')) |
---|
54 | |
---|
55 | def close(self): |
---|
56 | return self.buf.getvalue() |
---|
57 | |
---|
58 | |
---|
59 | class JSONTransportMixin: |
---|
60 | "json wrapper for xmlrpclib transport interfase" |
---|
61 | |
---|
62 | def send_content(self, connection, request_body): |
---|
63 | connection.putheader("Content-Type", "application/json") |
---|
64 | connection.putheader("Content-Length", str(len(request_body))) |
---|
65 | connection.endheaders() |
---|
66 | if request_body: |
---|
67 | connection.send(str.encode(request_body)) |
---|
68 | # todo: add gzip compression |
---|
69 | |
---|
70 | def getparser(self): |
---|
71 | # get parser and unmarshaller |
---|
72 | parser = JSONDummyParser() |
---|
73 | return parser, parser |
---|
74 | |
---|
75 | |
---|
76 | class JSONTransport(JSONTransportMixin, Transport): |
---|
77 | pass |
---|
78 | |
---|
79 | |
---|
80 | class JSONSafeTransport(JSONTransportMixin, SafeTransport): |
---|
81 | pass |
---|
82 | |
---|
83 | |
---|
84 | class ServerProxy(object): |
---|
85 | "JSON RPC Simple Client Service Proxy" |
---|
86 | |
---|
87 | def __init__(self, uri, transport=None, encoding=None, verbose=0, version=None, json_encoder=None): |
---|
88 | self.location = uri # server location (url) |
---|
89 | self.trace = verbose # show debug messages |
---|
90 | self.exceptions = True # raise errors? (JSONRPCError) |
---|
91 | self.timeout = None |
---|
92 | self.json_request = self.json_response = '' |
---|
93 | self.version = version # '2.0' for jsonrpc2 |
---|
94 | self.json_encoder = json_encoder # Allow for a custom JSON encoding class |
---|
95 | |
---|
96 | type, uri = urllib.splittype(uri) |
---|
97 | if type not in ("http", "https"): |
---|
98 | raise IOError("unsupported JSON-RPC protocol") |
---|
99 | self.__host, self.__handler = urllib.splithost(uri) |
---|
100 | |
---|
101 | if transport is None: |
---|
102 | if type == "https": |
---|
103 | transport = JSONSafeTransport() |
---|
104 | else: |
---|
105 | transport = JSONTransport() |
---|
106 | self.__transport = transport |
---|
107 | self.__encoding = encoding |
---|
108 | self.__verbose = verbose |
---|
109 | |
---|
110 | def __getattr__(self, attr): |
---|
111 | "pseudo method that can be called" |
---|
112 | return lambda *args, **vars: self.call(attr, *args, **vars) |
---|
113 | |
---|
114 | def call(self, method, *args, **vars): |
---|
115 | "JSON RPC communication (method invocation)" |
---|
116 | |
---|
117 | # build data sent to the service |
---|
118 | request_id = random.randint(0, sys.maxsize) |
---|
119 | data = {'id': request_id, 'method': method, 'params': args or vars, } |
---|
120 | if self.version: |
---|
121 | data['jsonrpc'] = self.version #mandatory key/value for jsonrpc2 validation else err -32600 |
---|
122 | request = json.dumps(data, cls=self.json_encoder) |
---|
123 | |
---|
124 | # make HTTP request (retry if connection is lost) |
---|
125 | response = self.__transport.request( |
---|
126 | self.__host, |
---|
127 | self.__handler, |
---|
128 | request, |
---|
129 | verbose=self.__verbose |
---|
130 | ) |
---|
131 | |
---|
132 | # store plain request and response for further debugging |
---|
133 | self.json_request = request |
---|
134 | self.json_response = response |
---|
135 | |
---|
136 | # parse json data coming from service |
---|
137 | # {'version': '1.1', 'id': id, 'result': result, 'error': None} |
---|
138 | response = json.loads(response) |
---|
139 | |
---|
140 | self.error = response.get('error', {}) |
---|
141 | if self.error and self.exceptions: |
---|
142 | raise JSONRPCError(self.error.get('code', 0), |
---|
143 | self.error.get('message', ''), |
---|
144 | self.error.get('data', None)) |
---|
145 | if response['id'] != request_id: |
---|
146 | raise JSONRPCError(0, "JSON Request ID != Response ID") |
---|
147 | |
---|
148 | return response.get('result') |
---|
149 | |
---|
150 | |
---|
151 | ServiceProxy = ServerProxy |
---|
152 | |
---|
153 | |
---|
154 | if __name__ == "__main__": |
---|
155 | # basic tests: |
---|
156 | location = "http://www.web2py.com.ar/webservices/sample/call/jsonrpc" |
---|
157 | client = ServerProxy(location, verbose='--verbose' in sys.argv,) |
---|
158 | print(client.add(1, 2)) |
---|