JSONRPCServerMiddleware: jsonrpcserver.py

File jsonrpcserver.py, 9.2 KB (added by alx3, 15 years ago)
Line 
1"""
2This Django middleware suits for building JSON-RPC servers.
3Configuration:
4 1) put class 'jsonrpcserver.JSONRPCServerMiddleware' to Django middleware list in settings.py
5 2) add 'jsonrpc_urlpatterns' to 'urls.py' with 'urlpatterns' syntax
6 example:
7 jsonrpc_urlpatterns = patterns('',
8 (r'^myapp/json_rpc_service/$', 'myapp.newapp.json_rpc_views'),
9 (r'^myapp/json_rpc_geoservice/$', 'myapp.geo.json_rpc_views'),
10 )
11 note: 'myapp.newapp.json_rpc_views' IS NOT A FUNCTION, BUT A MODULE, that
12 contains exposed JSON-RPC functions.
13 3) optional Django settings:
14 1. JSONRPC_URLPATTERNS_NAME:
15 Default value: 'jsonrpc_urlpatterns'.
16 Name of the variable in urls.py,that contains JSON-RPC URL patterns.
17 URL pattern must have Django syntax, but they must contain
18 MODULES NAMES, NOT FUNCTION NAMES as second element in tuple for patterns function.
19 2. JSONRPC_SERIALIZER:
20 Default value: JSONSerializer class - thin wrapper over django.utils.simplejson.
21 Name of the JSON serializer class, that must have methods
22 'serialize' and 'deserialize'
23 3. JSONRPC_JSONFY_AT_ALL_COSTS:
24 Default value: False.
25 Boolean flag, that determines whether JSON encoder should throwing an error or
26 returning a str() representation of unsupportable python object.
27 See implementation of AugmentedJSONEncoder class.
28Use:
29 1) write exposed functions:
30 from jsonrpcserver import jsonrpc_function
31 @jsonrpc_function
32 def my_python_method(par1, par2):
33 return par1 + par2
34 ...
35 2) use from JS client (e.g. dojo):
36 var service = new dojo.rpc.JsonService({
37 "serviceType": "JSON-RPC",
38 "serviceURL": "/myapp/json_rpc_service/"
39 });
40 service.callRemote("my_python_method", ["string1", "string2"])
41 .addCallback(...);
42Specifications (json-rpc.org):
43 input: '{"method":"my_python_method", "params": ["hello ", "world"], "id": 1}'
44 output: '{"error": null, "result": "hello world", "id": 1}'
45
46Written by alx3 (alx3apps(at)gmail(dot)com).
47Inspired by Java Struts2 JSON-plugin by Musachy Barroso and
48SimpleJSONRPCServer by David McNab.
49You can use this code under the terms of BSD licence (like Django project).
50"""
51
52import sys
53import traceback
54import django
55from django.http import HttpResponse
56import django.utils.simplejson as json
57
58
59_dispatchers_dict = {}
60
61def _import_module(mod_name):
62 """
63 Importing module through __import__ function
64 """
65 module = __import__(mod_name)
66 components = mod_name.split('.')
67 for comp in components[1:]:
68 module = getattr(module, comp)
69 return module
70
71def _import_class(class_path):
72 """
73 Importing class through __import__ function
74 """
75 path_list = class_path.split(".")
76 module_path = ".".join(path_list[:-1])
77 class_name = path_list[-1]
78 module = _import_module(module_path)
79 return getattr(module, class_name)
80
81class AugmentedJSONEncoder(json.JSONEncoder):
82 """
83 Augmentation for simplejson encoder.
84 Now additionally encodes arbitrary iterables, class instances and decimals.
85 """
86# switch this flag to True in production
87 if hasattr(django.conf.settings, "JSONRPC_JSONFY_AT_ALL_COSTS"):
88 _jsonfy_at_all_costs_flag = django.conf.settings.JSONRPC_JSONFY_AT_ALL_COSTS;
89 else:
90 _jsonfy_at_all_costs_flag = False;
91
92 def default(self, o):
93 if(hasattr(o, "__iter__")):
94 iterable = iter(o)
95 return list(iterable)
96 elif(hasattr(o, "__add__") and hasattr(o, "__sub__") and hasattr(o, "__mul__")):
97 return float(o)
98 elif(hasattr(o, "__class__")):
99 return o.__dict__
100 else:
101 if _jsonfy_at_all_costs_flag:
102 return str(o)
103 else:
104# JSON exception raised here
105 return json.JSONEncoder.default(self, o)
106
107class JSONSerializer:
108 """
109 JSON encoder/decoder wrapper
110 """
111 def serialize(self, obj):
112 return AugmentedJSONEncoder().encode(obj)
113 def deserialize(self, string):
114 return json.JSONDecoder().decode(string)
115
116
117class FunctionDispatcher(object):
118 """
119 Simple function dispatcher.
120 Dispatch syntax:
121 result = disp.dispatch("my_fun", *["param1", param2])
122 """
123 def __init__(self):
124 self.func_dict = {}
125
126 def register_function(self, func, func_name=None):
127 if not func_name:
128 func_name = func.func_name
129 #prevent overriding existing function
130 if(self.func_dict.has_key(func_name)):
131 raise Exception("Function '%s' already registered" % func_name)
132 self.func_dict[func_name] = func
133
134 def has_function(self, func_name):
135 return func_name in self.func_dict
136
137 def dispatch(self, func_name, *args, **kwargs):
138 if func_name in self.func_dict:
139 sought_func = self.func_dict[func_name]
140 else:
141 raise Exception("Function '%s' doesn't exist" % func_name)
142 response = sought_func(*args, **kwargs)
143 return response
144
145def jsonrpc_function(func):
146 """
147 Decorator for JSON-RPC method.
148 Server use:
149 @jsonrpc_function
150 def my_python_method(s1, s2):
151 return s1 + s2;
152 Client use (dojo):
153 rpcService.callRemote("my_python_method", ["string1", "string2"])
154 .addCallback(...);
155 """
156 if not _dispatchers_dict.has_key(func.__module__):
157 _dispatchers_dict[func.__module__] = FunctionDispatcher()
158 dispatcher = _dispatchers_dict[func.__module__]
159 if not dispatcher.has_function(func.func_name):
160 dispatcher.register_function(func)
161 return func
162
163class JSONRPCServerMiddleware(object):
164 """
165 Django middleware class. Put it Django middleware list in settings.py.
166 """
167 #searching for JSON-RPC urlpatterns
168 urls_mod_name = django.conf.settings.ROOT_URLCONF
169 urls_module = _import_module(urls_mod_name)
170 if hasattr(django.conf.settings, "JSONRPC_URLPATTERNS_NAME"):
171 urlpatterns_name = django.conf.settings.JSONRPC_URLPATTERNS_NAME
172 else:
173 urlpatterns_name = "jsonrpc_urlpatterns"
174 jsonrpc_urlpatterns = getattr(urls_module, urlpatterns_name)
175
176 def __init__(self):
177 """
178 Importing all modules, listed in jsonrpc_urlpatterns in urls.py.
179 Initialization needs for fill dispatchers with funcitons.
180 """
181 for pattern in self.jsonrpc_urlpatterns:
182 #django hack here, see django.core.urlresolvers.RegexUrlPattern class
183 module_name = pattern._callback_str
184 __import__(module_name)
185 #initializing serializer
186 if hasattr(django.conf.settings, "JSONRPC_SERIALIZER"):
187 serializer_class = _import_class(django.conf.settings.JSONRPC_SERIALIZER)
188 self.serializer = serializer_class()
189 else:
190 self.serializer = JSONSerializer()
191
192
193 def process_request(self, request):
194 """
195 Preprocesses all POST request to find out whether its remote call.
196 Processes remote call and returns HttpResponse as result
197 """
198 if(request.method == "GET"):
199 return
200 for pattern in self.jsonrpc_urlpatterns:
201 match = pattern.regex.search(request.path[1:])
202 if match:
203 # If there are any named groups, use those as kwargs, ignoring
204 # non-named groups. Otherwise, pass all non-named arguments as
205 # positional arguments.
206 kwargs = match.groupdict()
207 if kwargs:
208 args = ()
209 else:
210 args = match.groups()
211 # In both cases, pass any extra_kwargs as **kwargs.
212 kwargs.update(pattern.default_args)
213 result_str = self._dispatch_rpc_call(pattern._callback_str, request.raw_post_data, args, kwargs)
214 return HttpResponse(result_str)
215
216 def _dispatch_rpc_call(self, module_name, raw_post_data, args, kwargs):
217 response_dict = {}
218 try:
219 if module_name in _dispatchers_dict:
220 dispatcher = _dispatchers_dict[module_name]
221 else:
222 raise Exception("Module '%s' doesn't have JSON-RPC functions" % module_name)
223
224 call_data = self.serializer.deserialize(raw_post_data)
225 call_id = call_data.get("id", None)
226 if call_id:
227 response_dict["id"] = call_id
228 else:
229 #following JSON-RPC spec, it's a notification, not a request
230 return ""
231 func_name = str(call_data["method"])
232 func_params = list(call_data["params"])
233
234 args_list = list(args)
235 args_list.extend(func_params)
236 result = dispatcher.dispatch(func_name, *args_list, **kwargs)
237 response_dict['result'] = result
238 response_dict['error'] = None
239 except Exception, e:
240 error_dict = {
241 "name": str(sys.exc_info()[0]),
242 "message": str(e),
243 "stack": traceback.format_exc()
244 }
245 response_dict['error'] = error_dict
246 response_dict['result'] = None
247 return self.serializer.serialize(response_dict)
248
249
Back to Top