Merge lp:~jnaous/rpc4django/urlgroups into lp:rpc4django
- urlgroups
- Merge into main
Proposed by
Jad Naous
Status: | Needs review |
---|---|
Proposed branch: | lp:~jnaous/rpc4django/urlgroups |
Merge into: | lp:rpc4django |
Diff against target: |
714 lines (+279/-118) 11 files modified
example/urls.py (+3/-2) rpc4django/rpcdispatcher.py (+7/-46) rpc4django/tests/test_rpcdispatcher.py (+0/-18) rpc4django/tests/test_rpcviews.py (+79/-19) rpc4django/tests/test_urls.py (+12/-0) rpc4django/tests/testmod/__init__.py (+1/-1) rpc4django/tests/testmod/models.py (+5/-0) rpc4django/tests/testmod/testsubmod/__init__.py (+4/-0) rpc4django/tests/utils.py (+64/-0) rpc4django/utils.py (+20/-0) rpc4django/views.py (+84/-32) |
To merge this branch: | bzr merge lp:~jnaous/rpc4django/urlgroups |
Related bugs: | |
Related blueprints: |
Different URLs expose different methods
(Undefined)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
davidfischer | Pending | ||
Review via email: mp+41388@code.launchpad.net |
Commit message
Description of the change
I've implemented the blueprint for enforcing specific methods to be available at specific URLs. I've also written tests for it. I've also allowed arbitrary keyword args to be passed to RPC methods because this can be useful for passing parameters from the URL at which an RPC is called.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'example/urls.py' | |||
2 | --- example/urls.py 2010-03-29 05:04:00 +0000 | |||
3 | +++ example/urls.py 2010-11-20 07:51:10 +0000 | |||
4 | @@ -1,4 +1,5 @@ | |||
5 | 1 | from django.conf.urls.defaults import * | 1 | from django.conf.urls.defaults import * |
6 | 2 | from rpc4django.utils import rpc_url | ||
7 | 2 | 3 | ||
8 | 3 | # Uncomment the next two lines to enable the admin: | 4 | # Uncomment the next two lines to enable the admin: |
9 | 4 | # from django.contrib import admin | 5 | # from django.contrib import admin |
10 | @@ -14,6 +15,6 @@ | |||
11 | 14 | 15 | ||
12 | 15 | # Uncomment the next line to enable the admin: | 16 | # Uncomment the next line to enable the admin: |
13 | 16 | # (r'^admin/(.*)', admin.site.root), | 17 | # (r'^admin/(.*)', admin.site.root), |
16 | 17 | ('^$', 'rpc4django.views.serve_rpc_request'), | 18 | rpc_url('^$'), |
17 | 18 | ('^RPC2$', 'rpc4django.views.serve_rpc_request'), | 19 | rpc_url('^RPC2$'), |
18 | 19 | ) | 20 | ) |
19 | 20 | 21 | ||
20 | === modified file 'rpc4django/rpcdispatcher.py' | |||
21 | --- rpc4django/rpcdispatcher.py 2010-10-27 04:36:20 +0000 | |||
22 | +++ rpc4django/rpcdispatcher.py 2010-11-20 07:51:10 +0000 | |||
23 | @@ -8,7 +8,6 @@ | |||
24 | 8 | import inspect | 8 | import inspect |
25 | 9 | import platform | 9 | import platform |
26 | 10 | import pydoc | 10 | import pydoc |
27 | 11 | import types | ||
28 | 12 | import xmlrpclib | 11 | import xmlrpclib |
29 | 13 | from xmlrpclib import Fault | 12 | from xmlrpclib import Fault |
30 | 14 | from django.contrib.auth import authenticate, login, logout | 13 | from django.contrib.auth import authenticate, login, logout |
31 | @@ -43,25 +42,16 @@ | |||
32 | 43 | 42 | ||
33 | 44 | @rpcmethod() | 43 | @rpcmethod() |
34 | 45 | @rpcmethod(name='myns.myFuncName', signature=['int','int']) | 44 | @rpcmethod(name='myns.myFuncName', signature=['int','int']) |
36 | 46 | @rpcmethod(permission='add_group') | 45 | @rpcmethod(permission='add_group', url_name="my_url_name") |
37 | 47 | 46 | ||
38 | 48 | ''' | 47 | ''' |
39 | 49 | 48 | ||
40 | 50 | def set_rpcmethod_info(method): | 49 | def set_rpcmethod_info(method): |
41 | 51 | method.is_rpcmethod = True | 50 | method.is_rpcmethod = True |
55 | 52 | method.signature = [] | 51 | method.external_name = kwargs.get("name", getattr(method, '__name__')) |
56 | 53 | method.permission = None | 52 | method.signature = kwargs.get('signature', []) |
57 | 54 | method.external_name = getattr(method, '__name__') | 53 | method.permission = kwargs.get('permission', None) |
58 | 55 | 54 | method.url_name = kwargs.get("url_name", "root") | |
46 | 56 | if 'name' in kwargs: | ||
47 | 57 | method.external_name = kwargs['name'] | ||
48 | 58 | |||
49 | 59 | if 'signature' in kwargs: | ||
50 | 60 | method.signature = kwargs['signature'] | ||
51 | 61 | |||
52 | 62 | if 'permission' in kwargs: | ||
53 | 63 | method.permission = kwargs['permission'] | ||
54 | 64 | |||
59 | 65 | return method | 55 | return method |
60 | 66 | return set_rpcmethod_info | 56 | return set_rpcmethod_info |
61 | 67 | 57 | ||
62 | @@ -210,9 +200,9 @@ | |||
63 | 210 | 200 | ||
64 | 211 | ''' | 201 | ''' |
65 | 212 | 202 | ||
67 | 213 | def __init__(self, url='', apps=[], restrict_introspection=False, restrict_ootb_auth=True): | 203 | def __init__(self, url_name='root', restrict_introspection=False, restrict_ootb_auth=True): |
68 | 214 | version = platform.python_version_tuple() | 204 | version = platform.python_version_tuple() |
70 | 215 | self.url = url | 205 | self.url_name = url_name |
71 | 216 | self.rpcmethods = [] # a list of RPCMethod objects | 206 | self.rpcmethods = [] # a list of RPCMethod objects |
72 | 217 | self.jsonrpcdispatcher = JSONRPCDispatcher() | 207 | self.jsonrpcdispatcher = JSONRPCDispatcher() |
73 | 218 | self.xmlrpcdispatcher = XMLRPCDispatcher() | 208 | self.xmlrpcdispatcher = XMLRPCDispatcher() |
74 | @@ -226,8 +216,6 @@ | |||
75 | 226 | if not restrict_ootb_auth: | 216 | if not restrict_ootb_auth: |
76 | 227 | self.register_method(self.system_login) | 217 | self.register_method(self.system_login) |
77 | 228 | self.register_method(self.system_logout) | 218 | self.register_method(self.system_logout) |
78 | 229 | |||
79 | 230 | self.register_rpcmethods(apps) | ||
80 | 231 | 219 | ||
81 | 232 | @rpcmethod(name='system.describe', signature=['struct']) | 220 | @rpcmethod(name='system.describe', signature=['struct']) |
82 | 233 | def system_describe(self): | 221 | def system_describe(self): |
83 | @@ -237,7 +225,6 @@ | |||
84 | 237 | 225 | ||
85 | 238 | description = {} | 226 | description = {} |
86 | 239 | description['serviceType'] = 'RPC4Django JSONRPC+XMLRPC' | 227 | description['serviceType'] = 'RPC4Django JSONRPC+XMLRPC' |
87 | 240 | description['serviceURL'] = self.url, | ||
88 | 241 | description['methods'] = [{'name': method.name, | 228 | description['methods'] = [{'name': method.name, |
89 | 242 | 'summary': method.help, | 229 | 'summary': method.help, |
90 | 243 | 'params': method.get_params(), | 230 | 'params': method.get_params(), |
91 | @@ -314,32 +301,6 @@ | |||
92 | 314 | 301 | ||
93 | 315 | return False | 302 | return False |
94 | 316 | 303 | ||
95 | 317 | def register_rpcmethods(self, apps): | ||
96 | 318 | ''' | ||
97 | 319 | Scans the installed apps for methods with the rpcmethod decorator | ||
98 | 320 | Adds these methods to the list of methods callable via RPC | ||
99 | 321 | ''' | ||
100 | 322 | |||
101 | 323 | for appname in apps: | ||
102 | 324 | # check each app for any rpcmethods | ||
103 | 325 | app = __import__(appname, globals(), locals(), ['*']) | ||
104 | 326 | for obj in dir(app): | ||
105 | 327 | method = getattr(app, obj) | ||
106 | 328 | if callable(method) and \ | ||
107 | 329 | hasattr(method, 'is_rpcmethod') and \ | ||
108 | 330 | method.is_rpcmethod == True: | ||
109 | 331 | # if this method is callable and it has the rpcmethod | ||
110 | 332 | # decorator, add it to the dispatcher | ||
111 | 333 | self.register_method(method, method.external_name) | ||
112 | 334 | elif isinstance(method, types.ModuleType): | ||
113 | 335 | # if this is not a method and instead a sub-module, | ||
114 | 336 | # scan the module for methods with @rpcmethod | ||
115 | 337 | try: | ||
116 | 338 | self.register_rpcmethods(["%s.%s" % (appname, obj)]) | ||
117 | 339 | except ImportError: | ||
118 | 340 | pass | ||
119 | 341 | |||
120 | 342 | |||
121 | 343 | def jsondispatch(self, raw_post_data, **kwargs): | 304 | def jsondispatch(self, raw_post_data, **kwargs): |
122 | 344 | ''' | 305 | ''' |
123 | 345 | Sends the post data to :meth:`rpc4django.jsonrpcdispatcher.JSONRPCDispatcher.dispatch` | 306 | Sends the post data to :meth:`rpc4django.jsonrpcdispatcher.JSONRPCDispatcher.dispatch` |
124 | 346 | 307 | ||
125 | === modified file 'rpc4django/tests/test_rpcdispatcher.py' | |||
126 | --- rpc4django/tests/test_rpcdispatcher.py 2010-03-29 05:04:00 +0000 | |||
127 | +++ rpc4django/tests/test_rpcdispatcher.py 2010-11-20 07:51:10 +0000 | |||
128 | @@ -139,24 +139,6 @@ | |||
129 | 139 | self.assertEqual(jsondict['id'], 1) | 139 | self.assertEqual(jsondict['id'], 1) |
130 | 140 | self.assertEqual(jsondict['result'], 3) | 140 | self.assertEqual(jsondict['result'], 3) |
131 | 141 | 141 | ||
132 | 142 | def test_register_methods(self): | ||
133 | 143 | self.d.register_rpcmethods(['rpc4django.tests.testmod']) | ||
134 | 144 | |||
135 | 145 | jsontxt = '{"params":[3,1],"method":"subtract","id":1}' | ||
136 | 146 | resp = self.d.jsondispatch(jsontxt) | ||
137 | 147 | jsondict = json.loads(resp) | ||
138 | 148 | self.assertTrue(jsondict['error'] is None) | ||
139 | 149 | self.assertEqual(jsondict['id'], 1) | ||
140 | 150 | self.assertEqual(jsondict['result'], 2) | ||
141 | 151 | |||
142 | 152 | jsontxt = '{"params":[3,2],"method":"power","id":99}' | ||
143 | 153 | resp = self.d.jsondispatch(jsontxt) | ||
144 | 154 | jsondict = json.loads(resp) | ||
145 | 155 | self.assertTrue(jsondict['error'] is None) | ||
146 | 156 | self.assertEqual(jsondict['id'], 99) | ||
147 | 157 | self.assertEqual(jsondict['result'], 9) | ||
148 | 158 | |||
149 | 159 | |||
150 | 160 | def test_kwargs(self): | 142 | def test_kwargs(self): |
151 | 161 | self.d.register_method(self.kwargstest) | 143 | self.d.register_method(self.kwargstest) |
152 | 162 | 144 | ||
153 | 163 | 145 | ||
154 | === modified file 'rpc4django/tests/test_rpcviews.py' | |||
155 | --- rpc4django/tests/test_rpcviews.py 2010-03-29 05:04:00 +0000 | |||
156 | +++ rpc4django/tests/test_rpcviews.py 2010-11-20 07:51:10 +0000 | |||
157 | @@ -6,30 +6,57 @@ | |||
158 | 6 | 6 | ||
159 | 7 | import unittest | 7 | import unittest |
160 | 8 | import xmlrpclib | 8 | import xmlrpclib |
162 | 9 | from django.test.client import Client | 9 | from django.core.urlresolvers import reverse |
163 | 10 | from rpc4django.jsonrpcdispatcher import json, JSONRPC_SERVICE_ERROR | 10 | from rpc4django.jsonrpcdispatcher import json, JSONRPC_SERVICE_ERROR |
169 | 11 | 11 | from rpc4django import views | |
170 | 12 | RPCPATH = '/RPC2' | 12 | from rpc4django.tests.utils import SettingsTestCase |
171 | 13 | 13 | ||
172 | 14 | class TestRPCViews(unittest.TestCase): | 14 | |
173 | 15 | 15 | class TestRPCViews(SettingsTestCase): | |
174 | 16 | |||
175 | 17 | urls = "rpc4django.tests.test_urls" | ||
176 | 18 | |||
177 | 16 | def setUp(self): | 19 | def setUp(self): |
180 | 17 | self.client = Client() | 20 | self.settings_manager.set( |
181 | 18 | 21 | INSTALLED_APPS=( | |
182 | 22 | "rpc4django", | ||
183 | 23 | "rpc4django.tests.testmod", | ||
184 | 24 | ), | ||
185 | 25 | MIDDLEWARE_CLASSES=( | ||
186 | 26 | 'django.middleware.common.CommonMiddleware', | ||
187 | 27 | 'django.middleware.transaction.TransactionMiddleware', | ||
188 | 28 | 'django.middleware.csrf.CsrfViewMiddleware', | ||
189 | 29 | 'django.contrib.sessions.middleware.SessionMiddleware', | ||
190 | 30 | 'django.contrib.auth.middleware.AuthenticationMiddleware', | ||
191 | 31 | ), | ||
192 | 32 | DEBUG_PROPAGATE_EXCEPTIONS=False, | ||
193 | 33 | ) | ||
194 | 34 | |||
195 | 35 | views._register_rpcmethods( | ||
196 | 36 | [ | ||
197 | 37 | "rpc4django", | ||
198 | 38 | "rpc4django.tests.testmod", | ||
199 | 39 | ], | ||
200 | 40 | restrict_introspection=False, | ||
201 | 41 | dispatchers=views.dispatchers) | ||
202 | 42 | |||
203 | 43 | self.rpc_path = reverse("serve_rpc_request") | ||
204 | 44 | self.ns_rpc_path = reverse("my_url_name") | ||
205 | 45 | |||
206 | 19 | def test_methodsummary(self): | 46 | def test_methodsummary(self): |
208 | 20 | response = self.client.get(RPCPATH) | 47 | response = self.client.get(self.rpc_path) |
209 | 21 | self.assertEqual(response.status_code, 200) | 48 | self.assertEqual(response.status_code, 200) |
210 | 22 | self.assertEqual(response.template.name, 'rpc4django/rpcmethod_summary.html') | 49 | self.assertEqual(response.template.name, 'rpc4django/rpcmethod_summary.html') |
211 | 23 | 50 | ||
212 | 24 | def test_xmlrequests(self): | 51 | def test_xmlrequests(self): |
213 | 25 | data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>' | 52 | data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>' |
215 | 26 | response = self.client.post(RPCPATH, data, 'text/xml') | 53 | response = self.client.post(self.rpc_path, data, 'text/xml') |
216 | 27 | self.assertEqual(response.status_code, 200) | 54 | self.assertEqual(response.status_code, 200) |
217 | 28 | xmlrpclib.loads(response.content) # this will throw an exception with bad data | 55 | xmlrpclib.loads(response.content) # this will throw an exception with bad data |
218 | 29 | 56 | ||
219 | 30 | def test_jsonrequests(self): | 57 | def test_jsonrequests(self): |
220 | 31 | data = '{"params":[],"method":"system.listMethods","id":123}' | 58 | data = '{"params":[],"method":"system.listMethods","id":123}' |
222 | 32 | response = self.client.post(RPCPATH, data, 'application/json') | 59 | response = self.client.post(self.rpc_path, data, 'application/json') |
223 | 33 | self.assertEqual(response.status_code, 200) | 60 | self.assertEqual(response.status_code, 200) |
224 | 34 | jsondict = json.loads(response.content) | 61 | jsondict = json.loads(response.content) |
225 | 35 | self.assertTrue(jsondict['error'] is None) | 62 | self.assertTrue(jsondict['error'] is None) |
226 | @@ -37,7 +64,7 @@ | |||
227 | 37 | self.assertTrue(isinstance(jsondict['result'], list)) | 64 | self.assertTrue(isinstance(jsondict['result'], list)) |
228 | 38 | 65 | ||
229 | 39 | data = '{"params":[],"method":"system.describe","id":456}' | 66 | data = '{"params":[],"method":"system.describe","id":456}' |
231 | 40 | response = self.client.post(RPCPATH, data, 'text/javascript') | 67 | response = self.client.post(self.rpc_path, data, 'text/javascript') |
232 | 41 | self.assertEqual(response.status_code, 200) | 68 | self.assertEqual(response.status_code, 200) |
233 | 42 | jsondict = json.loads(response.content) | 69 | jsondict = json.loads(response.content) |
234 | 43 | self.assertTrue(jsondict['error'] is None) | 70 | self.assertTrue(jsondict['error'] is None) |
235 | @@ -46,7 +73,7 @@ | |||
236 | 46 | 73 | ||
237 | 47 | def test_typedetection(self): | 74 | def test_typedetection(self): |
238 | 48 | data = '{"params":[],"method":"system.listMethods","id":123}' | 75 | data = '{"params":[],"method":"system.listMethods","id":123}' |
240 | 49 | response = self.client.post(RPCPATH, data, 'text/plain') | 76 | response = self.client.post(self.rpc_path, data, 'text/plain') |
241 | 50 | self.assertEqual(response.status_code, 200) | 77 | self.assertEqual(response.status_code, 200) |
242 | 51 | jsondict = json.loads(response.content) | 78 | jsondict = json.loads(response.content) |
243 | 52 | self.assertTrue(jsondict['error'] is None) | 79 | self.assertTrue(jsondict['error'] is None) |
244 | @@ -54,13 +81,13 @@ | |||
245 | 54 | self.assertTrue(isinstance(jsondict['result'], list)) | 81 | self.assertTrue(isinstance(jsondict['result'], list)) |
246 | 55 | 82 | ||
247 | 56 | data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>' | 83 | data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>' |
249 | 57 | response = self.client.post(RPCPATH, data, 'text/plain') | 84 | response = self.client.post(self.rpc_path, data, 'text/plain') |
250 | 58 | self.assertEqual(response.status_code, 200) | 85 | self.assertEqual(response.status_code, 200) |
251 | 59 | xmlrpclib.loads(response.content) # this will throw an exception with bad data | 86 | xmlrpclib.loads(response.content) # this will throw an exception with bad data |
252 | 60 | 87 | ||
253 | 61 | # jsonrpc request with xmlrpc data (should be error) | 88 | # jsonrpc request with xmlrpc data (should be error) |
254 | 62 | data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>' | 89 | data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>' |
256 | 63 | response = self.client.post(RPCPATH, data, 'application/json') | 90 | response = self.client.post(self.rpc_path, data, 'application/json') |
257 | 64 | self.assertEqual(response.status_code, 200) | 91 | self.assertEqual(response.status_code, 200) |
258 | 65 | jsondict = json.loads(response.content) | 92 | jsondict = json.loads(response.content) |
259 | 66 | self.assertTrue(jsondict['result'] is None) | 93 | self.assertTrue(jsondict['result'] is None) |
260 | @@ -69,7 +96,7 @@ | |||
261 | 69 | 96 | ||
262 | 70 | data = '{"params":[],"method":"system.listMethods","id":123}' | 97 | data = '{"params":[],"method":"system.listMethods","id":123}' |
263 | 71 | try: | 98 | try: |
265 | 72 | response = self.client.post(RPCPATH, data, 'text/xml') | 99 | response = self.client.post(self.rpc_path, data, 'text/xml') |
266 | 73 | except: | 100 | except: |
267 | 74 | # for some reason, this throws an expat error | 101 | # for some reason, this throws an expat error |
268 | 75 | # but only in python 2.4 | 102 | # but only in python 2.4 |
269 | @@ -83,7 +110,7 @@ | |||
270 | 83 | 110 | ||
271 | 84 | def test_badrequests(self): | 111 | def test_badrequests(self): |
272 | 85 | data = '{"params":[],"method":"system.methodHelp","id":456}' | 112 | data = '{"params":[],"method":"system.methodHelp","id":456}' |
274 | 86 | response = self.client.post(RPCPATH, data, 'application/json') | 113 | response = self.client.post(self.rpc_path, data, 'application/json') |
275 | 87 | self.assertEqual(response.status_code, 200) | 114 | self.assertEqual(response.status_code, 200) |
276 | 88 | jsondict = json.loads(response.content) | 115 | jsondict = json.loads(response.content) |
277 | 89 | self.assertTrue(jsondict['error'] is not None) | 116 | self.assertTrue(jsondict['error'] is not None) |
278 | @@ -92,7 +119,7 @@ | |||
279 | 92 | self.assertEqual(jsondict['error']['code'], JSONRPC_SERVICE_ERROR) | 119 | self.assertEqual(jsondict['error']['code'], JSONRPC_SERVICE_ERROR) |
280 | 93 | 120 | ||
281 | 94 | data = '<?xml version="1.0"?><methodCall><methodName>method.N0t.Exists</methodName><params></params></methodCall>' | 121 | data = '<?xml version="1.0"?><methodCall><methodName>method.N0t.Exists</methodName><params></params></methodCall>' |
283 | 95 | response = self.client.post(RPCPATH, data, 'text/xml') | 122 | response = self.client.post(self.rpc_path, data, 'text/xml') |
284 | 96 | self.assertEqual(response.status_code, 200) | 123 | self.assertEqual(response.status_code, 200) |
285 | 97 | try: | 124 | try: |
286 | 98 | xmlrpclib.loads(response.content) | 125 | xmlrpclib.loads(response.content) |
287 | @@ -108,9 +135,42 @@ | |||
288 | 108 | # options requests can only be tested by django 1.1+ | 135 | # options requests can only be tested by django 1.1+ |
289 | 109 | self.fail('This version of django "%s" does not support http access control' %str(t)) | 136 | self.fail('This version of django "%s" does not support http access control' %str(t)) |
290 | 110 | 137 | ||
292 | 111 | response = self.client.options(RPCPATH, '', 'text/plain') | 138 | response = self.client.options(self.rpc_path, '', 'text/plain') |
293 | 112 | self.assertEqual(response['Access-Control-Allow-Methods'], 'POST, GET, OPTIONS') | 139 | self.assertEqual(response['Access-Control-Allow-Methods'], 'POST, GET, OPTIONS') |
294 | 113 | self.assertEqual(response['Access-Control-Max-Age'], '0') | 140 | self.assertEqual(response['Access-Control-Max-Age'], '0') |
295 | 114 | 141 | ||
296 | 142 | def test_good_url_name(self): | ||
297 | 143 | """ | ||
298 | 144 | Make sure we call functions based on the url they are arriving on. | ||
299 | 145 | """ | ||
300 | 146 | data = xmlrpclib.dumps((5, 4), "subtract") | ||
301 | 147 | response = self.client.post(self.rpc_path, data, 'text/xml') | ||
302 | 148 | self.assertEqual(response.status_code, 200) | ||
303 | 149 | result, methname = xmlrpclib.loads(response.content) | ||
304 | 150 | self.assertEqual(result, (1,)) | ||
305 | 151 | self.assertEqual(methname, None) | ||
306 | 152 | |||
307 | 153 | data = xmlrpclib.dumps((5, 4), "product") | ||
308 | 154 | response = self.client.post(self.ns_rpc_path, data, 'text/xml') | ||
309 | 155 | self.assertEqual(response.status_code, 200) | ||
310 | 156 | result, methname = xmlrpclib.loads(response.content) | ||
311 | 157 | self.assertEqual(result, (20,)) | ||
312 | 158 | self.assertEqual(methname, None) | ||
313 | 159 | |||
314 | 160 | def test_bad_url_name(self): | ||
315 | 161 | """ | ||
316 | 162 | Make sure we cannot call functions using the wrong url_name. | ||
317 | 163 | """ | ||
318 | 164 | data = xmlrpclib.dumps((5, 4), "subtract") | ||
319 | 165 | response = self.client.post(self.ns_rpc_path, data, 'text/xml') | ||
320 | 166 | |||
321 | 167 | self.assertEqual(response.status_code, 200) | ||
322 | 168 | try: | ||
323 | 169 | result, methname = xmlrpclib.loads(response.content) | ||
324 | 170 | self.fail("Expected xmlrpclib Fault") | ||
325 | 171 | except xmlrpclib.Fault as fault: | ||
326 | 172 | self.assertEqual(fault.faultCode, 1) | ||
327 | 173 | self.assertTrue(fault.faultString.endswith('method "subtract" is not supported')) | ||
328 | 174 | |||
329 | 115 | if __name__ == '__main__': | 175 | if __name__ == '__main__': |
330 | 116 | unittest.main() | 176 | unittest.main() |
331 | 117 | \ No newline at end of file | 177 | \ No newline at end of file |
332 | 118 | 178 | ||
333 | === added file 'rpc4django/tests/test_urls.py' | |||
334 | --- rpc4django/tests/test_urls.py 1970-01-01 00:00:00 +0000 | |||
335 | +++ rpc4django/tests/test_urls.py 2010-11-20 07:51:10 +0000 | |||
336 | @@ -0,0 +1,12 @@ | |||
337 | 1 | ''' | ||
338 | 2 | Created on Jun 9, 2010 | ||
339 | 3 | |||
340 | 4 | @author: jnaous | ||
341 | 5 | ''' | ||
342 | 6 | from django.conf.urls.defaults import * | ||
343 | 7 | from rpc4django.utils import rpc_url | ||
344 | 8 | |||
345 | 9 | urlpatterns = patterns("", | ||
346 | 10 | rpc_url(r"^RPC2/$", name="serve_rpc_request", use_name_for_dispatch=False), | ||
347 | 11 | rpc_url(r"^my_url/RPC2/$", name="my_url_name"), | ||
348 | 12 | ) | ||
349 | 0 | 13 | ||
350 | === modified file 'rpc4django/tests/testmod/__init__.py' | |||
351 | --- rpc4django/tests/testmod/__init__.py 2010-03-29 05:04:00 +0000 | |||
352 | +++ rpc4django/tests/testmod/__init__.py 2010-11-20 07:51:10 +0000 | |||
353 | @@ -5,6 +5,6 @@ | |||
354 | 5 | def subtract(a, b): | 5 | def subtract(a, b): |
355 | 6 | return a - b | 6 | return a - b |
356 | 7 | 7 | ||
358 | 8 | @rpcmethod(signature=['int', 'int', 'int']) | 8 | @rpcmethod(signature=['int', 'int', 'int'], url_name="my_url_name") |
359 | 9 | def product(a, b): | 9 | def product(a, b): |
360 | 10 | return a * b | 10 | return a * b |
361 | 11 | 11 | ||
362 | === added file 'rpc4django/tests/testmod/models.py' | |||
363 | --- rpc4django/tests/testmod/models.py 1970-01-01 00:00:00 +0000 | |||
364 | +++ rpc4django/tests/testmod/models.py 2010-11-20 07:51:10 +0000 | |||
365 | @@ -0,0 +1,5 @@ | |||
366 | 1 | ''' | ||
367 | 2 | Created on Jun 9, 2010 | ||
368 | 3 | |||
369 | 4 | @author: jnaous | ||
370 | 5 | ''' | ||
371 | 0 | 6 | ||
372 | === modified file 'rpc4django/tests/testmod/testsubmod/__init__.py' | |||
373 | --- rpc4django/tests/testmod/testsubmod/__init__.py 2010-03-29 05:04:00 +0000 | |||
374 | +++ rpc4django/tests/testmod/testsubmod/__init__.py 2010-11-20 07:51:10 +0000 | |||
375 | @@ -3,3 +3,7 @@ | |||
376 | 3 | @rpcmethod(signature=['int', 'int', 'int']) | 3 | @rpcmethod(signature=['int', 'int', 'int']) |
377 | 4 | def power(a, b): | 4 | def power(a, b): |
378 | 5 | return a ** b | 5 | return a ** b |
379 | 6 | |||
380 | 7 | @rpcmethod(signature=['int', 'int'], url_name="my_url_name") | ||
381 | 8 | def power2(a): | ||
382 | 9 | return 2 ** a | ||
383 | 6 | 10 | ||
384 | === added file 'rpc4django/tests/utils.py' | |||
385 | --- rpc4django/tests/utils.py 1970-01-01 00:00:00 +0000 | |||
386 | +++ rpc4django/tests/utils.py 2010-11-20 07:51:10 +0000 | |||
387 | @@ -0,0 +1,64 @@ | |||
388 | 1 | ''' | ||
389 | 2 | Created on Nov 19, 2010 | ||
390 | 3 | |||
391 | 4 | http://djangosnippets.org/snippets/1011/ | ||
392 | 5 | |||
393 | 6 | @author: jnaous | ||
394 | 7 | ''' | ||
395 | 8 | from django.conf import settings | ||
396 | 9 | from django.core.management import call_command | ||
397 | 10 | from django.db.models import loading | ||
398 | 11 | from django.test import TestCase | ||
399 | 12 | |||
400 | 13 | NO_SETTING = ('!', None) | ||
401 | 14 | |||
402 | 15 | class TestSettingsManager(object): | ||
403 | 16 | """ | ||
404 | 17 | A class which can modify some Django settings temporarily for a | ||
405 | 18 | test and then revert them to their original values later. | ||
406 | 19 | |||
407 | 20 | Automatically handles resyncing the DB if INSTALLED_APPS is | ||
408 | 21 | modified. | ||
409 | 22 | |||
410 | 23 | """ | ||
411 | 24 | def __init__(self): | ||
412 | 25 | self._original_settings = {} | ||
413 | 26 | |||
414 | 27 | def set(self, **kwargs): | ||
415 | 28 | for k,v in kwargs.iteritems(): | ||
416 | 29 | self._original_settings.setdefault(k, getattr(settings, k, | ||
417 | 30 | NO_SETTING)) | ||
418 | 31 | setattr(settings, k, v) | ||
419 | 32 | if 'INSTALLED_APPS' in kwargs: | ||
420 | 33 | self.syncdb() | ||
421 | 34 | |||
422 | 35 | def syncdb(self): | ||
423 | 36 | loading.cache.loaded = False | ||
424 | 37 | call_command('syncdb', verbosity=0) | ||
425 | 38 | |||
426 | 39 | def revert(self): | ||
427 | 40 | for k,v in self._original_settings.iteritems(): | ||
428 | 41 | if v == NO_SETTING: | ||
429 | 42 | delattr(settings, k) | ||
430 | 43 | else: | ||
431 | 44 | setattr(settings, k, v) | ||
432 | 45 | if 'INSTALLED_APPS' in self._original_settings: | ||
433 | 46 | self.syncdb() | ||
434 | 47 | self._original_settings = {} | ||
435 | 48 | |||
436 | 49 | |||
437 | 50 | class SettingsTestCase(TestCase): | ||
438 | 51 | """ | ||
439 | 52 | A subclass of the Django TestCase with a settings_manager | ||
440 | 53 | attribute which is an instance of TestSettingsManager. | ||
441 | 54 | |||
442 | 55 | Comes with a tearDown() method that calls | ||
443 | 56 | self.settings_manager.revert(). | ||
444 | 57 | |||
445 | 58 | """ | ||
446 | 59 | def __init__(self, *args, **kwargs): | ||
447 | 60 | super(SettingsTestCase, self).__init__(*args, **kwargs) | ||
448 | 61 | self.settings_manager = TestSettingsManager() | ||
449 | 62 | |||
450 | 63 | def tearDown(self): | ||
451 | 64 | self.settings_manager.revert() | ||
452 | 0 | 65 | ||
453 | === added file 'rpc4django/utils.py' | |||
454 | --- rpc4django/utils.py 1970-01-01 00:00:00 +0000 | |||
455 | +++ rpc4django/utils.py 2010-11-20 07:51:10 +0000 | |||
456 | @@ -0,0 +1,20 @@ | |||
457 | 1 | ''' | ||
458 | 2 | Created on Jun 9, 2010 | ||
459 | 3 | |||
460 | 4 | @author: jnaous | ||
461 | 5 | ''' | ||
462 | 6 | from django.conf.urls.defaults import url | ||
463 | 7 | |||
464 | 8 | def rpc_url(regex, kwargs=None, name=None, prefix='', | ||
465 | 9 | use_name_for_dispatch=True): | ||
466 | 10 | """ | ||
467 | 11 | Wrapper around C{django.conf.urls.defaults.url} to add the C{name} parameter | ||
468 | 12 | to C{kwargs} as "url_name", and to automatically assign the C{view}. | ||
469 | 13 | C{use_name_for_dispatch} controls whether or not to add C{name} to C{kwargs} | ||
470 | 14 | """ | ||
471 | 15 | if name != None and use_name_for_dispatch: | ||
472 | 16 | if kwargs == None: | ||
473 | 17 | kwargs = {} | ||
474 | 18 | kwargs["url_name"] = name | ||
475 | 19 | view = "rpc4django.views.serve_rpc_request" | ||
476 | 20 | return url(regex, view, kwargs, name, prefix) | ||
477 | 0 | 21 | ||
478 | === modified file 'rpc4django/views.py' | |||
479 | --- rpc4django/views.py 2010-10-27 04:36:20 +0000 | |||
480 | +++ rpc4django/views.py 2010-11-20 07:51:10 +0000 | |||
481 | @@ -1,23 +1,23 @@ | |||
482 | 1 | ''' | 1 | ''' |
483 | 2 | The main entry point for RPC4Django. Usually, the user simply puts | 2 | The main entry point for RPC4Django. Usually, the user simply puts |
485 | 3 | :meth:`serve_rpc_request <rpc4django.views.serve_rpc_request>` into ``urls.py`` | 3 | :meth:`rpc_url <rpc4django.utils.rpc_url>` into ``urls.py`` |
486 | 4 | 4 | ||
487 | 5 | :: | 5 | :: |
488 | 6 | 6 | ||
489 | 7 | urlpatterns = patterns('', | 7 | urlpatterns = patterns('', |
490 | 8 | # rpc4django will need to be in your Python path | 8 | # rpc4django will need to be in your Python path |
492 | 9 | (r'^RPC2$', 'rpc4django.views.serve_rpc_request'), | 9 | rpc_url(r'^RPC2$', name="my_url_name"), |
493 | 10 | ) | 10 | ) |
494 | 11 | 11 | ||
495 | 12 | ''' | 12 | ''' |
496 | 13 | 13 | ||
497 | 14 | import logging | 14 | import logging |
498 | 15 | import types | ||
499 | 15 | from xml.dom.minidom import parseString | 16 | from xml.dom.minidom import parseString |
500 | 16 | from xml.parsers.expat import ExpatError | 17 | from xml.parsers.expat import ExpatError |
501 | 17 | from django.http import HttpResponse, Http404, HttpResponseForbidden | 18 | from django.http import HttpResponse, Http404, HttpResponseForbidden |
502 | 18 | from django.shortcuts import render_to_response | 19 | from django.shortcuts import render_to_response |
503 | 19 | from django.conf import settings | 20 | from django.conf import settings |
504 | 20 | from django.core.urlresolvers import reverse, NoReverseMatch | ||
505 | 21 | from rpcdispatcher import RPCDispatcher | 21 | from rpcdispatcher import RPCDispatcher |
506 | 22 | from __init__ import version | 22 | from __init__ import version |
507 | 23 | 23 | ||
508 | @@ -45,7 +45,25 @@ | |||
509 | 45 | # these will be scanned for @rpcmethod decorators | 45 | # these will be scanned for @rpcmethod decorators |
510 | 46 | APPS = getattr(settings, 'INSTALLED_APPS', []) | 46 | APPS = getattr(settings, 'INSTALLED_APPS', []) |
511 | 47 | 47 | ||
513 | 48 | def check_request_permission(request, request_format='xml'): | 48 | logger = logging.getLogger("rpc4django.views") |
514 | 49 | |||
515 | 50 | class NonExistingDispatcher(Exception): | ||
516 | 51 | """Raised when the dispatcher for a particular name is not found.""" | ||
517 | 52 | def __init__(self, path, url_name): | ||
518 | 53 | super(NonExistingDispatcher, self).__init__( | ||
519 | 54 | "URL name '%s' is not used in any rpcmethod,\ | ||
520 | 55 | however, the URL '%s' uses it." % (url_name, path)) | ||
521 | 56 | self.path = path | ||
522 | 57 | self.url_name = url_name | ||
523 | 58 | |||
524 | 59 | def get_dispatcher(path, url_name): | ||
525 | 60 | try: | ||
526 | 61 | dispatcher = dispatchers[url_name] | ||
527 | 62 | except KeyError: | ||
528 | 63 | raise NonExistingDispatcher(path, url_name) | ||
529 | 64 | return dispatcher | ||
530 | 65 | |||
531 | 66 | def check_request_permission(request, request_format='xml', url_name="root"): | ||
532 | 49 | ''' | 67 | ''' |
533 | 50 | Checks whether this user has permission to call a particular method | 68 | Checks whether this user has permission to call a particular method |
534 | 51 | This method does not check method call validity. That is done later | 69 | This method does not check method call validity. That is done later |
535 | @@ -54,11 +72,13 @@ | |||
536 | 54 | 72 | ||
537 | 55 | - ``request`` - a django HttpRequest object | 73 | - ``request`` - a django HttpRequest object |
538 | 56 | - ``request_format`` - the request type: 'json' or 'xml' | 74 | - ``request_format`` - the request type: 'json' or 'xml' |
539 | 75 | - ``url_name`` - the name of the url at which this request was received | ||
540 | 57 | 76 | ||
541 | 58 | Returns ``False`` if permission is denied and ``True`` otherwise | 77 | Returns ``False`` if permission is denied and ``True`` otherwise |
542 | 59 | ''' | 78 | ''' |
543 | 60 | 79 | ||
544 | 61 | user = getattr(request, 'user', None) | 80 | user = getattr(request, 'user', None) |
545 | 81 | dispatcher = get_dispatcher(request.path, url_name) | ||
546 | 62 | methods = dispatcher.list_methods() | 82 | methods = dispatcher.list_methods() |
547 | 63 | method_name = dispatcher.get_method_name(request.raw_post_data, \ | 83 | method_name = dispatcher.get_method_name(request.raw_post_data, \ |
548 | 64 | request_format) | 84 | request_format) |
549 | @@ -69,20 +89,20 @@ | |||
550 | 69 | # this is the method the user is calling | 89 | # this is the method the user is calling |
551 | 70 | # time to check the permissions | 90 | # time to check the permissions |
552 | 71 | if method.permission is not None: | 91 | if method.permission is not None: |
554 | 72 | logging.debug('Method "%s" is protected by permission "%s"' \ | 92 | logger.debug('Method "%s" is protected by permission "%s"' \ |
555 | 73 | %(method.name, method.permission)) | 93 | %(method.name, method.permission)) |
556 | 74 | if user is None: | 94 | if user is None: |
557 | 75 | # user is only none if not using AuthenticationMiddleware | 95 | # user is only none if not using AuthenticationMiddleware |
559 | 76 | logging.warn('AuthenticationMiddleware is not enabled') | 96 | logger.warn('AuthenticationMiddleware is not enabled') |
560 | 77 | response = False | 97 | response = False |
561 | 78 | elif not user.has_perm(method.permission): | 98 | elif not user.has_perm(method.permission): |
562 | 79 | # check the permission against the permission database | 99 | # check the permission against the permission database |
564 | 80 | logging.info('User "%s" is NOT authorized' %(str(user))) | 100 | logger.info('User "%s" is NOT authorized' %(str(user))) |
565 | 81 | response = False | 101 | response = False |
566 | 82 | else: | 102 | else: |
568 | 83 | logging.debug('User "%s" is authorized' %(str(user))) | 103 | logger.debug('User "%s" is authorized' %(str(user))) |
569 | 84 | else: | 104 | else: |
571 | 85 | logging.debug('Method "%s" is unprotected' %(method.name)) | 105 | logger.debug('Method "%s" is unprotected' %(method.name)) |
572 | 86 | 106 | ||
573 | 87 | break | 107 | break |
574 | 88 | 108 | ||
575 | @@ -111,8 +131,8 @@ | |||
576 | 111 | return False | 131 | return False |
577 | 112 | 132 | ||
578 | 113 | if LOG_REQUESTS_RESPONSES: | 133 | if LOG_REQUESTS_RESPONSES: |
581 | 114 | logging.info('Unrecognized content-type "%s"' %conttype) | 134 | logger.info('Unrecognized content-type "%s"' %conttype) |
582 | 115 | logging.info('Analyzing rpc request data to get content type') | 135 | logger.info('Analyzing rpc request data to get content type') |
583 | 116 | 136 | ||
584 | 117 | # analyze post data to see whether it is xml or json | 137 | # analyze post data to see whether it is xml or json |
585 | 118 | # this is slower than if the content-type was set properly | 138 | # this is slower than if the content-type was set properly |
586 | @@ -124,7 +144,7 @@ | |||
587 | 124 | 144 | ||
588 | 125 | return False | 145 | return False |
589 | 126 | 146 | ||
591 | 127 | def serve_rpc_request(request): | 147 | def serve_rpc_request(request, url_name="root", **kwargs): |
592 | 128 | ''' | 148 | ''' |
593 | 129 | Handles rpc calls based on the content type of the request or | 149 | Handles rpc calls based on the content type of the request or |
594 | 130 | returns the method documentation page if the request | 150 | returns the method documentation page if the request |
595 | @@ -134,38 +154,42 @@ | |||
596 | 134 | 154 | ||
597 | 135 | ``request`` | 155 | ``request`` |
598 | 136 | the Django HttpRequest object | 156 | the Django HttpRequest object |
600 | 137 | 157 | ``url_name`` | |
601 | 158 | the name of the url at which the request arrived | ||
602 | 159 | |||
603 | 138 | ''' | 160 | ''' |
605 | 139 | 161 | ||
606 | 162 | dispatcher = get_dispatcher(request.path, url_name) | ||
607 | 163 | |||
608 | 140 | if request.method == "POST" and len(request.POST) > 0: | 164 | if request.method == "POST" and len(request.POST) > 0: |
609 | 141 | # Handle POST request with RPC payload | 165 | # Handle POST request with RPC payload |
610 | 142 | 166 | ||
611 | 143 | if LOG_REQUESTS_RESPONSES: | 167 | if LOG_REQUESTS_RESPONSES: |
613 | 144 | logging.debug('Incoming request: %s' %str(request.raw_post_data)) | 168 | logger.debug('Incoming request: %s' % str(request.raw_post_data)) |
614 | 145 | 169 | ||
615 | 146 | if is_xmlrpc_request(request): | 170 | if is_xmlrpc_request(request): |
616 | 147 | if RESTRICT_XML: | 171 | if RESTRICT_XML: |
617 | 148 | raise Http404 | 172 | raise Http404 |
618 | 149 | 173 | ||
620 | 150 | if not check_request_permission(request, 'xml'): | 174 | if not check_request_permission(request, 'xml', url_name=url_name): |
621 | 151 | return HttpResponseForbidden() | 175 | return HttpResponseForbidden() |
622 | 152 | 176 | ||
625 | 153 | resp = dispatcher.xmldispatch(request.raw_post_data, \ | 177 | resp = dispatcher.xmldispatch(request.raw_post_data, |
626 | 154 | request=request) | 178 | request=request, **kwargs) |
627 | 155 | response_type = 'text/xml' | 179 | response_type = 'text/xml' |
628 | 156 | else: | 180 | else: |
629 | 157 | if RESTRICT_JSON: | 181 | if RESTRICT_JSON: |
630 | 158 | raise Http404 | 182 | raise Http404 |
631 | 159 | 183 | ||
633 | 160 | if not check_request_permission(request, 'json'): | 184 | if not check_request_permission(request, 'json', url_name=url_name): |
634 | 161 | return HttpResponseForbidden() | 185 | return HttpResponseForbidden() |
635 | 162 | 186 | ||
638 | 163 | resp = dispatcher.jsondispatch(request.raw_post_data, \ | 187 | resp = dispatcher.jsondispatch(request.raw_post_data, |
639 | 164 | request=request) | 188 | request=request, **kwargs) |
640 | 165 | response_type = 'application/json' | 189 | response_type = 'application/json' |
641 | 166 | 190 | ||
642 | 167 | if LOG_REQUESTS_RESPONSES: | 191 | if LOG_REQUESTS_RESPONSES: |
644 | 168 | logging.debug('Outgoing %s response: %s' %(response_type, resp)) | 192 | logger.debug('Outgoing %s response: %s' %(response_type, resp)) |
645 | 169 | 193 | ||
646 | 170 | return HttpResponse(resp, response_type) | 194 | return HttpResponse(resp, response_type) |
647 | 171 | elif request.method == 'OPTIONS': | 195 | elif request.method == 'OPTIONS': |
648 | @@ -185,7 +209,7 @@ | |||
649 | 185 | request.META.get('HTTP_ACCESS_CONTROL_REQUEST_HEADERS', '') | 209 | request.META.get('HTTP_ACCESS_CONTROL_REQUEST_HEADERS', '') |
650 | 186 | 210 | ||
651 | 187 | if LOG_REQUESTS_RESPONSES: | 211 | if LOG_REQUESTS_RESPONSES: |
653 | 188 | logging.debug('Outgoing HTTP access response to: %s' %(origin)) | 212 | logger.debug('Outgoing HTTP access response to: %s' %(origin)) |
654 | 189 | 213 | ||
655 | 190 | return response | 214 | return response |
656 | 191 | else: | 215 | else: |
657 | @@ -199,7 +223,7 @@ | |||
658 | 199 | methods = dispatcher.list_methods() | 223 | methods = dispatcher.list_methods() |
659 | 200 | template_data = { | 224 | template_data = { |
660 | 201 | 'methods': methods, | 225 | 'methods': methods, |
662 | 202 | 'url': URL, | 226 | 'url': request.path, |
663 | 203 | 227 | ||
664 | 204 | # rpc4django version | 228 | # rpc4django version |
665 | 205 | 'version': version(), | 229 | 'version': version(), |
666 | @@ -225,13 +249,41 @@ | |||
667 | 225 | if csrf_exempt is not None: | 249 | if csrf_exempt is not None: |
668 | 226 | serve_rpc_request = csrf_exempt(serve_rpc_request) | 250 | serve_rpc_request = csrf_exempt(serve_rpc_request) |
669 | 227 | 251 | ||
675 | 228 | # reverse the method for use with system.describe and ajax | 252 | def _register_rpcmethods(apps, restrict_introspection=False, restrict_ootb_auth=True, dispatchers={}): |
676 | 229 | try: | 253 | ''' |
677 | 230 | URL = reverse(serve_rpc_request) | 254 | Scans the installed apps for methods with the rpcmethod decorator |
678 | 231 | except NoReverseMatch: | 255 | Adds these methods to the list of methods callable via RPC |
679 | 232 | URL = '' | 256 | ''' |
680 | 233 | 257 | ||
684 | 234 | # instantiate the rpcdispatcher -- this examines the INSTALLED_APPS | 258 | for appname in apps: |
685 | 235 | # for any @rpcmethod decorators and adds them to the callable methods | 259 | # check each app for any rpcmethods |
686 | 236 | dispatcher = RPCDispatcher(URL, APPS, RESTRICT_INTROSPECTION, RESTRICT_OOTB_AUTH) | 260 | app = __import__(appname, globals(), locals(), ['*']) |
687 | 261 | for obj in dir(app): | ||
688 | 262 | method = getattr(app, obj) | ||
689 | 263 | if callable(method) and \ | ||
690 | 264 | getattr(method, 'is_rpcmethod', False) == True: | ||
691 | 265 | # if this method is callable and it has the rpcmethod | ||
692 | 266 | # decorator, add it to the dispatcher | ||
693 | 267 | if method.url_name not in dispatchers: | ||
694 | 268 | logger.debug("Registered URL name '%s'" % method.url_name) | ||
695 | 269 | dispatchers[method.url_name] = RPCDispatcher( | ||
696 | 270 | method.url_name, restrict_introspection, | ||
697 | 271 | restrict_ootb_auth) | ||
698 | 272 | logger.debug( | ||
699 | 273 | "Registered method '%s' to URL name '%s'" | ||
700 | 274 | % (method.external_name, method.url_name)) | ||
701 | 275 | dispatchers[method.url_name].register_method( | ||
702 | 276 | method, method.external_name) | ||
703 | 277 | elif isinstance(method, types.ModuleType): | ||
704 | 278 | # if this is not a method and instead a sub-module, | ||
705 | 279 | # scan the module for methods with @rpcmethod | ||
706 | 280 | try: | ||
707 | 281 | _register_rpcmethods( | ||
708 | 282 | ["%s.%s" % (appname, obj)], | ||
709 | 283 | restrict_introspection, restrict_ootb_auth, dispatchers) | ||
710 | 284 | except ImportError: | ||
711 | 285 | pass | ||
712 | 286 | return dispatchers | ||
713 | 287 | |||
714 | 288 | dispatchers = _register_rpcmethods(APPS, RESTRICT_INTROSPECTION, RESTRICT_OOTB_AUTH) | ||
715 | 237 | 289 |