-
Notifications
You must be signed in to change notification settings - Fork 22
/
rpc.py
181 lines (164 loc) · 6.14 KB
/
rpc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# -*- coding: utf-8 -*-
# Copyright © 2012-2014 by its contributors. See AUTHORS for details.
# Distributed under the MIT/X11 software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import time
from random import randrange
from hashlib import sha1
import requests
import six
try:
import json
except:
try:
import simplejson as json
except:
import django.utils.simplejson as json
from .tools import BytesIO
from .numeric import mpd
from .serialize import LittleInteger
__all__ = [
'ResponseError',
'Fault',
'boolean',
'Boolean',
'True',
'False',
'RESPONSE',
'dumps',
'loads',
'Proxy',
]
#
# :exc:`ResponseError` is raised in the case of a malformed response.
#
class ResponseError(Exception):
"Indicates a broken response package."
pass
#
# :exc:`Fault` represents a JSON-RPC fault code and descriptive string. It is
# used as an exception when a single request-response returns an error, and in
# batch processing and elsewhere as a representation of JSON-RPC fault codes.
#
class Fault(Exception):
"Indicates an JSON-RPC error code."
def __init__(self, faultCode=-1, faultString=None, faultDescription=None,
*args, **kwargs):
if faultString is None: faultString = ''
super(Fault, self).__init__(self, *args, **kwargs)
self.faultCode = faultCode
self.faultString = faultString
self.faultDescription = faultDescription
def __repr__(self):
if self.faultDescription is None:
return "<Fault %s: %s>" % (self.faultCode, repr(self.faultString))
else:
return "<Fault %s: %s (%s)>" % (self.faultCode,
repr(self.faultString), repr(self.faultDescription))
#
# boolean(value)
#
# Convert any Python value to one of the JSON-RPC Boolean constants,
# :const:`True` or :const:`False`.
#
from sys import modules
mod_dict = modules[__name__].__dict__
boolean = Boolean = bool
mod_dict['True'] = True
mod_dict['False'] = False
del modules, mod_dict
#
# dumps(id, method, params)
#
# Convert *params* into a numbered JSON-RPC request, or into a response if
# *method* is :const:`RESPONSE`. *params* can be either a list, tuple, or
# dictionary of arguments, a single JSON-representable value, or an instance
# of the :exc:`Fault` exception class.
#
RESPONSE = object()
def dumps(id, method, params):
jsonargs = dict(jsonrpc='2.0',id=id)
if isinstance(params, Fault):
jsonargs.update({'error': {
'code': params.faultCode,
'message': params.faultString,
}})
if params.faultDescription is not None:
jsonargs['error'].update({'data': params.faultDescription})
elif method is RESPONSE:
jsonargs.update({'result': params})
else:
jsonargs.update({'method': method})
jsonargs.update({'params': params})
return json.dumps(jsonargs, ensure_ascii=False)
#
# loads(payload)
#
# Convert an JSON-RPC request or response into Python objects, an `(id,
# method, paramsresponse)` tuple.
#
def loads(payload):
return json.loads(payload, parse_float=mpd)
#
# A proxy object manages communication with a remote bitcoind JSON-RPC server.
#
class Proxy(object):
def __init__(self, uri, username=None, password=None, service=None,
timeout=15, *args, **kwargs):
if username is None: username = 'rpcuser'
if password is None: password = ''
super(Proxy, self).__init__(*args, **kwargs)
self._ctr = 0
self.uri = uri
self.service = service
self.username = username
self.password = password
self.timeout = timeout
def __getattr__(self, name):
if self.service is not None:
name = '.'.join([self.service, name])
return self.__class__(self.uri, self.username, self.password, name,
self.timeout)
def __call__(self, *args, **kwargs):
if args and kwargs:
raise ValueError(
u"JSON-RPC allows encoding of either positional or keyword "
u"parameters, but not both")
if self.service is None:
raise ValueError(u"must specify JSON-RPC method")
# Generate a pseudo-random numeric identifier for this request:
self._ctr = (self._ctr+1) & 0xffffffff
hash_ = sha1(b''.join([
self.uri.encode('utf-8'),
self.service.encode('utf-8'),
self.username.encode('utf-8'),
LittleInteger(self._ctr).serialize(4),
time.ctime().encode('utf-8'),
LittleInteger(randrange(2**32)).serialize(4)])).digest()
id_ = LittleInteger.deserialize(BytesIO(hash_[:4]), 4)
# Execute the JSON-RPC call:
payload = dumps(id_, self.service, kwargs or args)
headers = {'Content-Type' : 'application/json'}
reply = requests.post(self.uri, auth=(self.username, self.password),
data=payload, timeout=self.timeout)
if reply.status_code >= 500:
reply.raise_for_status()
response = loads(reply.content)
# Process the response object:
#if 'jsonrpc' not in response:
# raise ResponseError(u"server reply was not JSON-RPC object")
if 'id' not in response or response['id'] != id_:
raise ResponseError(u"response id does not match request id")
if ('error' in response and response['error'] is not None and
'result' in response and response['result'] is not None):
raise ResponseError(u"server reply may not contain both 'result' and 'error'")
if 'error' in response and response['error'] is not None:
raise Fault(
'code' in response['error'] and response['error']['code'] or -1,
'message' in response['error'] and response['error']['message'] or '',
'description' in response['error'] and response['error']['description'] or None)
elif 'result' in response:
return response['result']
else:
raise ResponseError(u"server reply must contain one of 'result' or 'error'")
# End of File