Ticket #5732: multicall2.diff
| File multicall2.diff, 19.5 KB (added by braudel, 7 months ago) |
|---|
-
twisted/web/xmlrpc.py
319 319 ['string', 'string']] 320 320 321 321 322 @withRequest 323 def xmlrpc_multicall(self, request, procedureList): 324 """ 325 Execute several RPC methods in a single XMLRPC request using the 326 multicall object. 327 328 Example:: 329 On the server side, just load the instrospection so your 330 server has system.multicall. Then on the client:: 331 332 from twisted.web.xmlrpc import Proxy 333 from twisted.web.xmlrpc import MultiCall 334 335 proxy = Proxy('url of your server') 336 337 multiRPC = MultiCall( proxy ) 338 # queue a few calls 339 multiRPC.system.listMethods() 340 multiRPC.system.methodHelp('system.listMethods') 341 multiRPC.system.methodSignature('system.listMethods') 342 343 def handleResults(results): 344 for success, result in results: 345 print result 346 347 multiRPC().addCallback(handleResults) 348 349 @param request: The http C{request} object, obtained 350 via the @withRequest decorator. 351 @type request: L{http.Request} 352 353 @param procedureList: A list of dictionaries, each representing an 354 individual rpc call, containing the C{methodName} and the 355 C{params}. 356 @type procedureList: list 357 358 @return: L{defer.DeferredList} of the deferreds for each procedure 359 in procedure list. 360 @rtype: L{defer.DeferredList} 361 @since: 12.3 362 """ 363 def callError(error): 364 """ 365 Errorback to handle individual call errors. 366 367 Individual errors in a multicall are returned as 368 dictionaries. See U{http://www.xmlrpc.com/discuss/msgReader$1208}. 369 370 @param result: C{failure} 371 @type result: L{Failure} 372 373 @rtype: dict 374 @return: A dict with keys C{faultCode} and C{faultString}. 375 """ 376 log.err(error.value) 377 return {'faultCode': self.FAILURE, 378 'faultString': (error.value.faultString 379 if isinstance(error.value, Fault) 380 else getattr(error.value, 'message', ''))} 381 382 def prepareCallResponse(result): 383 """ 384 Callback to convert a call C{response} to a list. 385 386 The xmlrpc multicall spec expects a list wrapping 387 each call response. 388 See U{http://www.xmlrpc.com/discuss/msgReader$1208}. 389 390 @param result: C{response} 391 @type result: Any python type. 392 393 @rtype: list. 394 @return: A list with response as element 0. 395 """ 396 return [result] 397 398 def run(procedurePath, params): 399 """ 400 Run an individual procedure from the L{procedureList} and 401 returns a C{deferred}. 402 403 @param procedurePath: String naming a procedure. 404 @type procedurePath: str 405 406 @param params: List of arguments to be passed to the procedure. 407 @type params: list 408 409 @return: A C{deferred} object with prepareCallResponse and 410 callError attached. 411 @rtype: L{defer.Deferred} 412 """ 413 try: 414 procedure = self._xmlrpc_parent.lookupProcedure(procedurePath) 415 except NoSuchFunction, e: 416 return defer.fail(e).addErrback(callError) 417 else: 418 if getattr(procedure, 'withRequest', False): 419 call = defer.maybeDeferred(procedure, request, *params) 420 else: 421 call = defer.maybeDeferred(procedure, *params) 422 423 call.addCallback(prepareCallResponse) 424 call.addErrback(callError) 425 return call 426 427 results = [ 428 run(procedure['methodName'], procedure['params']) 429 for procedure in procedureList] 430 431 return (defer.DeferredList(results) 432 .addCallback(lambda results: [r[1] for r in results])) 433 434 xmlrpc_multicall.signature = [['array', 'array']] 435 436 437 438 class _DeferredMultiCallProcedure(object): 439 """ 440 A helper object to store calls made on the 441 MultiCall object for batch execution. 442 @since: 12.3 443 """ 444 def __init__(self, call_list, name): 445 self.__call_list = call_list 446 self.__name = name 447 448 449 def __getattr__(self, name): 450 """ 451 Magic to emulate x.y.name lookups for 452 a remote procedure. 453 """ 454 return _DeferredMultiCallProcedure( 455 self.__call_list, 456 "%s.%s" % (self.__name, name) 457 ) 458 459 460 def __call__(self, *args): 461 """ 462 "Calling" an RPC on the multicall queues a deferred, 463 the procedure name, and its calling args. 464 465 @return: A L{defer.Deferred} that will be fired with the 466 results for this RPC. 467 @rtype: L{defer.Deferred} 468 """ 469 d = defer.Deferred() 470 self.__call_list.append((d, self.__name, args)) 471 return d 472 473 474 475 class MultiCall(xmlrpclib.MultiCall): 476 """ 477 @param server: An object used to boxcar method calls 478 @type server: L{Proxy}. 479 480 @return: A L{defer.DeferredList} of all the deferreds for 481 each queued rpc call. 482 @rtype: L{defer.DeferredList} 483 @since: 12.3 484 485 Methods can be added to the MultiCall using normal 486 method call syntax e.g.:: 487 488 proxy = Proxy('http://advogato.org/XMLRPC') 489 490 multicall = MultiCall(proxy) 491 d1 = multicall.add(2,3) 492 d2 = multicall.add(5,6) 493 494 Or using the classic twisted L{Proxy} api:: 495 496 d3 = multicall.callRemote('add', 2, 3) 497 498 To execute the multicall, call the MultiCall object 499 and attach callbacks, errbacks to the returned 500 deferred.List e.g.:: 501 502 def printResults(results): 503 for result in results: 504 print result[1] 505 506 d = multicall() 507 d.addCallback(printResults) 508 """ 509 def __getattr__(self, name): 510 """ Get a ref to a helper object to emulate 511 RPC 'attributes lookup'. 512 """ 513 return _DeferredMultiCallProcedure(self.__call_list, name) 514 515 516 def callRemote(self, method, *args): 517 """ 518 Queue a call for C{method} on this multicall object 519 with the given arguments. 520 521 @return: A L{defer.Deferred} that will fire with the method response, 522 or a failure if the method raised a L{Fault}. 523 """ 524 return getattr(self, method)(*args) 525 526 527 def __call__(self): 528 """ 529 Execute the multicall, processing the deferreds for each 530 procedure once the results are ready. 531 532 @return: A L{defer.DeferredList} that will fire all the queued deferreds. 533 """ 534 marshalled_list = [] 535 deferreds = [] 536 for deferred, name, args in self.__call_list: 537 marshalled_list.append({ 538 'methodName': name, 539 'params': args}) 540 deferreds.append(deferred) 541 542 def processResults(results, deferreds): 543 """ 544 Callback to trigger the deferreds with their 545 corresponding RPC's results. 546 """ 547 for d, result in zip(deferreds, results): 548 if isinstance(result, dict): 549 d.errback(Fault(result['faultCode'], 550 result['faultString'])) 551 552 elif isinstance(result, list): 553 d.callback(result[0]) 554 555 else: 556 raise ValueError( 557 "Unexpected type in multicall result.") 558 559 self.__server.callRemote( 560 'system.multicall', marshalled_list 561 ).addCallback(processResults, deferreds) 562 563 return defer.DeferredList(deferreds) 564 565 322 566 def addIntrospection(xmlrpc): 323 567 """ 324 568 Add Introspection support to an XMLRPC server. … … 587 831 __all__ = [ 588 832 "XMLRPC", "Handler", "NoSuchFunction", "Proxy", 589 833 590 "Fault", "Binary", "Boolean", "DateTime" ]834 "Fault", "Binary", "Boolean", "DateTime", "MultiCall"] -
twisted/web/test/test_xmlrpc.py
14 14 from twisted.web import xmlrpc 15 15 from twisted.web.xmlrpc import ( 16 16 XMLRPC, payloadTemplate, addIntrospection, _QueryFactory, Proxy, 17 withRequest )17 withRequest, MultiCall) 18 18 from twisted.web import server, static, client, error, http 19 19 from twisted.internet import reactor, defer 20 20 from twisted.internet.error import ConnectionDone … … 28 28 else: 29 29 sslSkip = None 30 30 31 32 31 class AsyncXMLRPCTests(unittest.TestCase): 33 32 """ 34 33 Tests for L{XMLRPC}'s support of Deferreds. … … 667 666 return d 668 667 669 668 669 670 670 class XMLRPCTestIntrospection(XMLRPCTestCase): 671 671 672 672 def setUp(self): … … 686 686 'deferFault', 'dict', 'echo', 'fail', 'fault', 687 687 'pair', 'system.listMethods', 688 688 'system.methodHelp', 689 'system.methodSignature', 'withRequest']) 689 'system.methodSignature', 'system.multicall', 690 'withRequest']) 690 691 691 692 d = self.proxy().callRemote("system.listMethods") 692 693 d.addCallback(cbMethods) … … 720 721 return defer.DeferredList(dl, fireOnOneErrback=True) 721 722 722 723 724 725 class FakeProxy(object): 726 """ 727 Fake twisted XMLRPC Proxy client to run tests without using 728 the network 729 """ 730 def __init__(self, resource): 731 self.resource = resource 732 733 734 def callRemote(self, methodName, *args): 735 """ 736 emulate twisted.web.xmlrpc.Proxy.callRemote 737 """ 738 # build request 739 request = DummyRequest(['']) 740 request.method = 'POST' 741 request.content = StringIO( 742 payloadTemplate % (methodName, xmlrpclib.dumps(args))) 743 744 def returnResponse( requestResponse ): 745 results = xmlrpclib.loads(requestResponse)[0] 746 if len(results) == 1: 747 results = results[0] 748 return results 749 750 # look mom no network! 751 self.resource.render(request) 752 753 return (defer.succeed("".join(request.written)) 754 .addCallback(returnResponse)) 755 756 757 758 class XMLRPCTestMultiCall(unittest.TestCase): 759 """ 760 Tests for xmlrpc multicalls 761 """ 762 def setUp(self): 763 self.resource = Test() 764 addIntrospection(self.resource) 765 self.proxy = FakeProxy(self.resource) 766 767 768 def test_multicall(self): 769 """ 770 test a suscessfull multicall 771 """ 772 inputs = range(5) 773 m = MultiCall(self.proxy) 774 for x in inputs: 775 m.echo(x) 776 777 def testResults(results): 778 self.assertEqual(inputs, [x[1] for x in results]) 779 780 resultsDeferred = m().addCallback(testResults) 781 self.assertTrue(resultsDeferred.called) 782 783 784 def test_multicall_callRemote(self): 785 """ 786 test a suscessfull multicall using 787 multicall.callRemote instead of attribute lookups 788 """ 789 inputs = range(5) 790 m = MultiCall(self.proxy) 791 for x in inputs: 792 m.callRemote('echo', x) 793 794 def testResults(results): 795 self.assertEqual(inputs, [x[1] for x in results]) 796 797 resultsDeferred = m().addCallback(testResults) 798 self.assertTrue(resultsDeferred.called) 799 800 801 def test_multicall_with_callbacks(self): 802 """ 803 test correct execution of callbacks added to the 804 multicall's returned deferreds for each individual queued 805 call 806 """ 807 inputs = range(5) 808 m = MultiCall(self.proxy) 809 for x in inputs: 810 d = m.echo(x) 811 d.addCallback( lambda x : x*x ) 812 813 def testResults(results): 814 self.assertEqual([ x*x for x in inputs], [x[1] for x in results]) 815 816 resultsDeferred = m().addCallback(testResults) 817 self.assertTrue(resultsDeferred.called) 818 819 820 def test_multicall_errorback(self): 821 """ 822 test that an error (an invalid - not found - method) 823 does not propagate if properly handled in the errorback 824 of an individual deferred 825 """ 826 def trapFoo(error): 827 error.trap(xmlrpclib.Fault) 828 self.assertEqual(error.value.faultString, 829 'procedure foo not found', 830 'check we have a failure message' 831 ) 832 self.flushLoggedErrors(xmlrpc.NoSuchFunction) 833 834 835 m = MultiCall(self.proxy) 836 m.echo(1) 837 # method not present on server 838 m.foo().addErrback(trapFoo) 839 m.echo(2) 840 841 def handleErrors(error): 842 error.trap(xmlrpclib.Fault) 843 self.assertEqual(error.value.faultString, 844 'xmlrpc_echo() takes exactly 2 arguments (4 given)') 845 self.flushLoggedErrors(TypeError) 846 847 m.echo(1,2,3).addErrback(handleErrors) 848 849 def testResults(results): 850 """ 851 the errorback should have trapped the error 852 """ 853 self.assertEqual(results[1], (True, None), 854 'failure trapped in errorback does not propagate to deferredList results') 855 856 resultsDeferred = m().addCallback(testResults) 857 self.assertTrue(resultsDeferred.called) 858 859 860 def test_multicall_withRequest(self): 861 """ 862 Test that methods decorated with @withRequest are handled correctly 863 """ 864 m = MultiCall(self.proxy) 865 m.echo(1) 866 # method decorated with withRequest 867 msg = 'hoho' 868 m.withRequest(msg) 869 m.echo(2) 870 871 def testResults(results): 872 """ 873 test that a withRequest decorated method was properly handled 874 """ 875 self.assertEqual(results[1][1], 876 'POST %s' % msg, 'check withRequest decorated result') 877 878 resultsDeferred = m().addCallback(testResults) 879 self.assertTrue(resultsDeferred.called) 880 881 882 def test_multicall_with_xmlrpclib(self): 883 """ 884 check that the sever's response is also compatible with xmlrpclib 885 MultiCall client 886 """ 887 class PatchedXmlrpclibProxy(object): 888 """ 889 A proxy that more closely resembles xmlrpclib.ServerProxy 890 """ 891 def __init__(self, resource): 892 self.resource = resource 893 894 def __request(self, methodName, params): 895 """ 896 Patched xmlrpclib.ServerProxy.__request to emulate 897 RPC call without using the network 898 """ 899 request = DummyRequest(['']) 900 request.method = 'POST' 901 request.content = StringIO( 902 payloadTemplate % (methodName, xmlrpclib.dumps(params))) 903 904 self.resource.render(request) 905 response = xmlrpclib.loads("".join(request.written))[0] 906 if len(response) == 1: 907 response = response[0] 908 return response 909 910 def __getattr__(self, name): 911 """ 912 magic method dispatcher 913 """ 914 return xmlrpclib._Method(self.__request, name) 915 916 inputs = range(5) 917 m = xmlrpclib.MultiCall( 918 PatchedXmlrpclibProxy(self.resource)) 919 for x in inputs: 920 m.echo(x) 921 922 self.assertEqual( 923 inputs, 924 list(m()), 925 'xmlrpclib multicall can talk to the twisted multicall') 926 927 928 723 929 class XMLRPCClientErrorHandling(unittest.TestCase): 724 930 """ 725 931 Test error handling on the xmlrpc client. -
doc/web/howto/xmlrpc.xhtml
234 234 no <code class="python">help</code> attribute is specified, the 235 235 method's documentation string is used instead.</p> 236 236 237 238 237 239 <h2>SOAP Support</h2> 238 240 239 241 <p>From the point of view of a Twisted developer, there is little difference … … 295 297 [8, 15] 296 298 </pre> 297 299 300 <h3>Using multicall Objects</h3> 301 <p>Another informal pattern commonly used along with xmlrpc 302 introspection is the use of a multicall object to boxcar several 303 rpc calls in a single http request. 304 305 An instance of a <code base="twisted.web.xmlrpc" class="API">MultiCall</code> object is created on the client side and several RPC calls are queued by 306 calling them on this <code base="twisted.web.xmlrpc" class="API">MultiCall</code> instance. Every RPC called on the <code base="twisted.web.xmlrpc" class="API">MultiCall</code> returns a <code class="API" base="twisted.internet.defer">Deferred</code> that you can use to attach callbacks to each queued rpc response. 307 308 Executing the <code base="twisted.web.xmlrpc" class="API">MultiCall</code> instance itself triggers a request to 309 <code class="python">system.multicall</code> (<code class="API" base="twisted.web.xmlrpc">XMLRPCIntrospection.multicall</code>) with a list of dictionaries representing each of the procedures to call, returning a <code class="API" base="twisted.internet.defer">DeferredList</code> of the deferreds corresponding to each individual call. 310 311 Let's see an example to talk to the server above using a <code base="twisted.web.xmlrpc" class="API">MultiCall</code>:</p> 312 313 <pre class="python"> 314 from twisted.web.xmlrpc import Proxy, MultiCall, NoSuchFunction 315 from twisted.internet import reactor 316 317 def printResults(results): 318 for result in results: 319 print result[1] 320 reactor.stop() 321 322 proxy = Proxy('http://127.0.0.1:7080') 323 324 multiRPC = MultiCall(proxy) 325 326 # queue a few echo calls, with a callback to square the results 327 for x in range(10): 328 multiRPC.echo(x).addCallback(lambda x: x*x) 329 330 # you can of course use the <code>callRemote</code> method instead 331 # for a better twisted-like feeling 332 for x in range(10,20): 333 multiRPC.callRemote('echo', x).addCallback(lambda x: x*x) 334 335 # now queue a few more echo calls with a callback that will 336 # indicate the echoed letter and their index in the original string 337 for index, x in enumerate('hello'): 338 multiRPC.echo(x).addCallback(lambda result, index: '%s found at %d' % (result, index)) 339 340 # individual deferreds ideally should also handle errors 341 def handleErrors(error): 342 error.trap(xmlrpclib.Fault) 343 print "yay! The server said %s" % error.value.faultString 344 multiRPC.foo().addErrback(handleErrors) 345 346 # finally execute the multiRPC instance, to send the request, 347 # then handle the returned DeferredList 348 # results with the printResults callback 349 multiRPC().addCallback(printResults) 350 reactor.run() 351 </pre> 352 353 The <code class="API">twisted.web.XMLRPCIntrospection.multicall</code> method is also 100% compatible with the xmlrpclib MultiCall, so you can also use xmlrpclib as the client: 354 355 <pre class="python"> 356 from xmlrpclib import ServerProxy, Multicall 357 358 proxy = ServerProxy('http://127.0.0.1:7080') 359 360 multiRPC = MultiCall(proxy) 361 for x in range(10): 362 multiRPC.echo(x) 363 364 for result in multiRPC(): 365 print result * result 366 </pre> 367 368 298 369 <h2>Serving SOAP and XML-RPC simultaneously</h2> 299 370 300 371 <p><code class="API">twisted.web.xmlrpc.XMLRPC</code> and <code
