Skip to content

Commit

Permalink
Support XML-RPC multicall (#3778)
Browse files Browse the repository at this point in the history
  • Loading branch information
di authored and dstufft committed Apr 25, 2018
1 parent f7d6a63 commit 754c88a
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 0 deletions.
109 changes: 109 additions & 0 deletions tests/unit/legacy/api/xmlrpc/test_xmlrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,3 +792,112 @@ def test_browse(db_request):
"Programming Language :: Python",
],
)) == {(expected_release.name, expected_release.version)}


class TestMulticall:

def test_multicall(self, monkeypatch):
dumped = pretend.stub(encode=lambda: None)
dumps = pretend.call_recorder(lambda *a, **kw: dumped)
monkeypatch.setattr(xmlrpc.xmlrpc.client, 'dumps', dumps)

loaded = pretend.stub()
loads = pretend.call_recorder(lambda *a, **kw: loaded)
monkeypatch.setattr(xmlrpc.xmlrpc.client, 'loads', loads)

subreq = pretend.stub()
blank = pretend.call_recorder(lambda *a, **kw: subreq)
monkeypatch.setattr(xmlrpc.Request, 'blank', blank)

body = pretend.stub()
response = pretend.stub(body=body)

request = pretend.stub(
invoke_subrequest=pretend.call_recorder(lambda *a, **kw: response),
add_response_callback=pretend.call_recorder(
lambda *a, **kw: response),
)

callback = pretend.stub()
monkeypatch.setattr(
xmlrpc,
'measure_response_content_length',
pretend.call_recorder(lambda metric_name: callback)
)

args = [
{'methodName': 'search', 'params': [{'name': 'foo'}]},
{'methodName': 'browse', 'params': [{'classifiers': ['bar']}]},
]

responses = xmlrpc.multicall(request, args)

assert responses == [loaded, loaded]
assert blank.calls == [
pretend.call('/RPC2', headers={'Content-Type': 'text/xml'}),
pretend.call('/RPC2', headers={'Content-Type': 'text/xml'}),
]
assert request.invoke_subrequest.calls == [
pretend.call(subreq, use_tweens=True),
pretend.call(subreq, use_tweens=True),
]
assert request.add_response_callback.calls == [
pretend.call(callback),
]
assert dumps.calls == [
pretend.call(({'name': 'foo'},), methodname='search'),
pretend.call(({'classifiers': ['bar']},), methodname='browse'),
]
assert loads.calls == [pretend.call(body), pretend.call(body)]

def test_recursive_multicall(self):
request = pretend.stub()
args = [
{'methodName': 'system.multicall', 'params': []},
]
with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc:
xmlrpc.multicall(request, args)

assert exc.value.faultString == (
'ValueError: Cannot use system.multicall inside a multicall'
)

def test_missing_multicall_method(self):
request = pretend.stub()
args = [{}]
with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc:
xmlrpc.multicall(request, args)

assert exc.value.faultString == (
'ValueError: Method name not provided'
)

def test_too_many_multicalls_method(self):
request = pretend.stub()
args = [{'methodName': 'nah'}] * 21

with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc:
xmlrpc.multicall(request, args)

assert exc.value.faultString == (
'ValueError: Multicall limit is 20 calls'
)

def test_measure_response_content_length(self):
metric_name = 'some_metric_name'
callback = xmlrpc.measure_response_content_length(metric_name)

request = pretend.stub(
registry=pretend.stub(
datadog=pretend.stub(
histogram=pretend.call_recorder(lambda *a: None)
)
)
)
response = pretend.stub(content_length=pretend.stub())

callback(request, response)

assert request.registry.datadog.histogram.calls == [
pretend.call(metric_name, response.content_length),
]
51 changes: 51 additions & 0 deletions warehouse/legacy/api/xmlrpc/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
import collections.abc
import datetime
import functools
import xmlrpc.client
import xmlrpc.server

from elasticsearch_dsl import Q
from packaging.utils import canonicalize_name
from pyramid.request import Request
from pyramid.view import view_config
from pyramid_rpc.xmlrpc import (
exception_view as _exception_view, xmlrpc_method as _xmlrpc_method
Expand All @@ -31,6 +33,9 @@
)


_MAX_MULTICALLS = 20


def xmlrpc_method(**kwargs):
"""
Support multiple endpoints serving the same views by chaining calls to
Expand Down Expand Up @@ -441,3 +446,49 @@ def browse(request, classifiers):
)

return [(r.name, r.version) for r in releases]


def measure_response_content_length(metric_name):

def _callback(request, response):
request.registry.datadog.histogram(
metric_name, response.content_length
)

return _callback


@xmlrpc_method(method='system.multicall')
def multicall(request, args):
if any(arg.get('methodName') == 'system.multicall' for arg in args):
raise XMLRPCWrappedError(
ValueError('Cannot use system.multicall inside a multicall')
)

if not all(arg.get('methodName') for arg in args):
raise XMLRPCWrappedError(ValueError('Method name not provided'))

if len(args) > _MAX_MULTICALLS:
raise XMLRPCWrappedError(
ValueError(f'Multicall limit is {_MAX_MULTICALLS} calls')
)

responses = []
for arg in args:
name = arg.get('methodName')
subreq = Request.blank('/RPC2', headers={'Content-Type': 'text/xml'})
subreq.method = 'POST'
subreq.body = xmlrpc.client.dumps(
tuple(arg.get('params')),
methodname=name,
).encode()
response = request.invoke_subrequest(subreq, use_tweens=True)
responses.append(xmlrpc.client.loads(response.body))

request.add_response_callback(
measure_response_content_length(
'warehouse.xmlrpc.system.multicall.content_length'
)
)

return responses

0 comments on commit 754c88a

Please sign in to comment.