Skip to content

Commit 754c88a

Browse files
didstufft
authored andcommitted
Support XML-RPC multicall (#3778)
1 parent f7d6a63 commit 754c88a

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed

tests/unit/legacy/api/xmlrpc/test_xmlrpc.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,3 +792,112 @@ def test_browse(db_request):
792792
"Programming Language :: Python",
793793
],
794794
)) == {(expected_release.name, expected_release.version)}
795+
796+
797+
class TestMulticall:
798+
799+
def test_multicall(self, monkeypatch):
800+
dumped = pretend.stub(encode=lambda: None)
801+
dumps = pretend.call_recorder(lambda *a, **kw: dumped)
802+
monkeypatch.setattr(xmlrpc.xmlrpc.client, 'dumps', dumps)
803+
804+
loaded = pretend.stub()
805+
loads = pretend.call_recorder(lambda *a, **kw: loaded)
806+
monkeypatch.setattr(xmlrpc.xmlrpc.client, 'loads', loads)
807+
808+
subreq = pretend.stub()
809+
blank = pretend.call_recorder(lambda *a, **kw: subreq)
810+
monkeypatch.setattr(xmlrpc.Request, 'blank', blank)
811+
812+
body = pretend.stub()
813+
response = pretend.stub(body=body)
814+
815+
request = pretend.stub(
816+
invoke_subrequest=pretend.call_recorder(lambda *a, **kw: response),
817+
add_response_callback=pretend.call_recorder(
818+
lambda *a, **kw: response),
819+
)
820+
821+
callback = pretend.stub()
822+
monkeypatch.setattr(
823+
xmlrpc,
824+
'measure_response_content_length',
825+
pretend.call_recorder(lambda metric_name: callback)
826+
)
827+
828+
args = [
829+
{'methodName': 'search', 'params': [{'name': 'foo'}]},
830+
{'methodName': 'browse', 'params': [{'classifiers': ['bar']}]},
831+
]
832+
833+
responses = xmlrpc.multicall(request, args)
834+
835+
assert responses == [loaded, loaded]
836+
assert blank.calls == [
837+
pretend.call('/RPC2', headers={'Content-Type': 'text/xml'}),
838+
pretend.call('/RPC2', headers={'Content-Type': 'text/xml'}),
839+
]
840+
assert request.invoke_subrequest.calls == [
841+
pretend.call(subreq, use_tweens=True),
842+
pretend.call(subreq, use_tweens=True),
843+
]
844+
assert request.add_response_callback.calls == [
845+
pretend.call(callback),
846+
]
847+
assert dumps.calls == [
848+
pretend.call(({'name': 'foo'},), methodname='search'),
849+
pretend.call(({'classifiers': ['bar']},), methodname='browse'),
850+
]
851+
assert loads.calls == [pretend.call(body), pretend.call(body)]
852+
853+
def test_recursive_multicall(self):
854+
request = pretend.stub()
855+
args = [
856+
{'methodName': 'system.multicall', 'params': []},
857+
]
858+
with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc:
859+
xmlrpc.multicall(request, args)
860+
861+
assert exc.value.faultString == (
862+
'ValueError: Cannot use system.multicall inside a multicall'
863+
)
864+
865+
def test_missing_multicall_method(self):
866+
request = pretend.stub()
867+
args = [{}]
868+
with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc:
869+
xmlrpc.multicall(request, args)
870+
871+
assert exc.value.faultString == (
872+
'ValueError: Method name not provided'
873+
)
874+
875+
def test_too_many_multicalls_method(self):
876+
request = pretend.stub()
877+
args = [{'methodName': 'nah'}] * 21
878+
879+
with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc:
880+
xmlrpc.multicall(request, args)
881+
882+
assert exc.value.faultString == (
883+
'ValueError: Multicall limit is 20 calls'
884+
)
885+
886+
def test_measure_response_content_length(self):
887+
metric_name = 'some_metric_name'
888+
callback = xmlrpc.measure_response_content_length(metric_name)
889+
890+
request = pretend.stub(
891+
registry=pretend.stub(
892+
datadog=pretend.stub(
893+
histogram=pretend.call_recorder(lambda *a: None)
894+
)
895+
)
896+
)
897+
response = pretend.stub(content_length=pretend.stub())
898+
899+
callback(request, response)
900+
901+
assert request.registry.datadog.histogram.calls == [
902+
pretend.call(metric_name, response.content_length),
903+
]

warehouse/legacy/api/xmlrpc/views.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
import collections.abc
1414
import datetime
1515
import functools
16+
import xmlrpc.client
1617
import xmlrpc.server
1718

1819
from elasticsearch_dsl import Q
1920
from packaging.utils import canonicalize_name
21+
from pyramid.request import Request
2022
from pyramid.view import view_config
2123
from pyramid_rpc.xmlrpc import (
2224
exception_view as _exception_view, xmlrpc_method as _xmlrpc_method
@@ -31,6 +33,9 @@
3133
)
3234

3335

36+
_MAX_MULTICALLS = 20
37+
38+
3439
def xmlrpc_method(**kwargs):
3540
"""
3641
Support multiple endpoints serving the same views by chaining calls to
@@ -441,3 +446,49 @@ def browse(request, classifiers):
441446
)
442447

443448
return [(r.name, r.version) for r in releases]
449+
450+
451+
def measure_response_content_length(metric_name):
452+
453+
def _callback(request, response):
454+
request.registry.datadog.histogram(
455+
metric_name, response.content_length
456+
)
457+
458+
return _callback
459+
460+
461+
@xmlrpc_method(method='system.multicall')
462+
def multicall(request, args):
463+
if any(arg.get('methodName') == 'system.multicall' for arg in args):
464+
raise XMLRPCWrappedError(
465+
ValueError('Cannot use system.multicall inside a multicall')
466+
)
467+
468+
if not all(arg.get('methodName') for arg in args):
469+
raise XMLRPCWrappedError(ValueError('Method name not provided'))
470+
471+
if len(args) > _MAX_MULTICALLS:
472+
raise XMLRPCWrappedError(
473+
ValueError(f'Multicall limit is {_MAX_MULTICALLS} calls')
474+
)
475+
476+
responses = []
477+
for arg in args:
478+
name = arg.get('methodName')
479+
subreq = Request.blank('/RPC2', headers={'Content-Type': 'text/xml'})
480+
subreq.method = 'POST'
481+
subreq.body = xmlrpc.client.dumps(
482+
tuple(arg.get('params')),
483+
methodname=name,
484+
).encode()
485+
response = request.invoke_subrequest(subreq, use_tweens=True)
486+
responses.append(xmlrpc.client.loads(response.body))
487+
488+
request.add_response_callback(
489+
measure_response_content_length(
490+
'warehouse.xmlrpc.system.multicall.content_length'
491+
)
492+
)
493+
494+
return responses

0 commit comments

Comments
 (0)