diff --git a/CHANGES/2948.feature b/CHANGES/2948.feature new file mode 100644 index 00000000000..2d97b4c39a3 --- /dev/null +++ b/CHANGES/2948.feature @@ -0,0 +1 @@ +Added and links property for ClientResponse object diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index b0a1f5e342f..249cacc2137 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -687,6 +687,37 @@ def history(self): """A sequence of of responses, if redirects occurred.""" return self._history + @property + def links(self): + links_str = ", ".join(self.headers.getall("link", [])) + + links = MultiDict() + + if not links_str: + return MultiDictProxy(links) + + for val in re.split(r",(?=\s*<)", links_str): + url, params = re.match(r"\s*<(.*)>(.*)", val).groups() + params = params.split(";")[1:] + + link = MultiDict() + + for param in params: + key, _, value, _ = re.match( + r"^\s*(\S*)\s*=\s*(['\"]?)(.*?)(\2)\s*$", + param, re.M + ).groups() + + link.add(key, value) + + key = link.get("rel", url) + + link.add("url", self.url.join(URL(url))) + + links.add(key, MultiDictProxy(link)) + + return MultiDictProxy(links) + async def start(self, connection, read_until_eof=False): """Start response processing.""" self._closed = False diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 0a9db4ac851..19d87da830e 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -1097,6 +1097,16 @@ Response object Unmodified HTTP headers of response as unconverted bytes, a sequence of ``(key, value)`` pairs. + .. attribute:: links + + Link HTTP header parsed into a :class:`~multidict.MultiDictProxy`. + + For each link, key is link param `rel` when it exists, or link url as + :class:`str` otherwise, and value is :class:`~multidict.MultiDictProxy` + of link params and url at key `url` as :class:`~yarl.URL` instance. + + .. versionadded:: 3.2 + .. attribute:: content_type Read-only property with *content* part of *Content-Type* header. diff --git a/tests/test_client_response.py b/tests/test_client_response.py index 2ab2d5bf7ff..8eefa4b9fce 100644 --- a/tests/test_client_response.py +++ b/tests/test_client_response.py @@ -6,6 +6,7 @@ from unittest import mock import pytest +from multidict import CIMultiDict from yarl import URL import aiohttp @@ -1033,3 +1034,159 @@ def test_response_real_url(loop, session): session=session) assert response.url == url.with_fragment(None) assert response.real_url == url + + +def test_response_links_comma_separated(loop, session): + url = URL('http://def-cl-resp.org/') + response = ClientResponse('get', url, + request_info=mock.Mock(), + writer=mock.Mock(), + continue100=None, + timer=TimerNoop(), + auto_decompress=True, + traces=[], + loop=loop, + session=session) + response.headers = CIMultiDict([ + ( + "Link", + ('; rel=next, ' + '; rel=home') + ) + ]) + assert ( + response.links == + {'next': + {'url': URL('http://example.com/page/1.html'), + 'rel': 'next'}, + 'home': + {'url': URL('http://example.com/'), + 'rel': 'home'} + } + ) + + +def test_response_links_multiple_headers(loop, session): + url = URL('http://def-cl-resp.org/') + response = ClientResponse('get', url, + request_info=mock.Mock(), + writer=mock.Mock(), + continue100=None, + timer=TimerNoop(), + auto_decompress=True, + traces=[], + loop=loop, + session=session) + response.headers = CIMultiDict([ + ( + "Link", + '; rel=next' + ), + ( + "Link", + '; rel=home' + ) + ]) + assert ( + response.links == + {'next': + {'url': URL('http://example.com/page/1.html'), + 'rel': 'next'}, + 'home': + {'url': URL('http://example.com/'), + 'rel': 'home'} + } + ) + + +def test_response_links_no_rel(loop, session): + url = URL('http://def-cl-resp.org/') + response = ClientResponse('get', url, + request_info=mock.Mock(), + writer=mock.Mock(), + continue100=None, + timer=TimerNoop(), + auto_decompress=True, + traces=[], + loop=loop, + session=session) + response.headers = CIMultiDict([ + ( + "Link", + '' + ) + ]) + assert ( + response.links == + { + 'http://example.com/': + {'url': URL('http://example.com/')} + } + ) + + +def test_response_links_quoted(loop, session): + url = URL('http://def-cl-resp.org/') + response = ClientResponse('get', url, + request_info=mock.Mock(), + writer=mock.Mock(), + continue100=None, + timer=TimerNoop(), + auto_decompress=True, + traces=[], + loop=loop, + session=session) + response.headers = CIMultiDict([ + ( + "Link", + '; rel="home-page"' + ), + ]) + assert ( + response.links == + {'home-page': + {'url': URL('http://example.com/'), + 'rel': 'home-page'} + } + ) + + +def test_response_links_relative(loop, session): + url = URL('http://def-cl-resp.org/') + response = ClientResponse('get', url, + request_info=mock.Mock(), + writer=mock.Mock(), + continue100=None, + timer=TimerNoop(), + auto_decompress=True, + traces=[], + loop=loop, + session=session) + response.headers = CIMultiDict([ + ( + "Link", + '; rel=rel' + ), + ]) + assert ( + response.links == + {'rel': + {'url': URL('http://def-cl-resp.org/relative/path'), + 'rel': 'rel'} + } + ) + + +def test_response_links_empty(loop, session): + url = URL('http://def-cl-resp.org/') + response = ClientResponse('get', url, + request_info=mock.Mock(), + writer=mock.Mock(), + continue100=None, + timer=TimerNoop(), + auto_decompress=True, + traces=[], + loop=loop, + session=session) + response.headers = CIMultiDict() + assert response.links == {}