From: euri10 Date: Fri, 16 Aug 2019 14:07:52 +0000 (+0200) Subject: Added links property to BaseResponse (#211) X-Git-Tag: 0.7.0~8 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=cad9c035008bafd32cb47c50b07c5bfe7868f0f3;p=thirdparty%2Fhttpx.git Added links property to BaseResponse (#211) * Added docstring and better typing for parsed_header_links * Added docstring and better typing for links property Added tests for it too * Added --diff to black check to see where CI fails Relaxed travis build on master only * Fixed black issue removing a \t * Put noxfile and travis.yml as they were before --- diff --git a/httpx/models.py b/httpx/models.py index 2ffc290e..e433320e 100644 --- a/httpx/models.py +++ b/httpx/models.py @@ -34,6 +34,7 @@ from .utils import ( is_known_encoding, normalize_header_key, normalize_header_value, + parse_header_links, str_query_param, ) @@ -836,6 +837,20 @@ class BaseResponse: self._cookies.extract_cookies(self) return self._cookies + @property + def links(self) -> typing.Dict[typing.Optional[str], typing.Dict[str, str]]: + """ + Returns the parsed header links of the response, if any + """ + header = self.headers.get("link") + ldict = {} + if header: + links = parse_header_links(header) + for link in links: + key = link.get("rel") or link.get("url") + ldict[key] = link + return ldict + def __repr__(self) -> str: return f"" diff --git a/httpx/utils.py b/httpx/utils.py index ff399356..69613263 100644 --- a/httpx/utils.py +++ b/httpx/utils.py @@ -1,6 +1,7 @@ import codecs import netrc import os +import re import typing from pathlib import Path @@ -102,3 +103,40 @@ def get_netrc_login(host: str) -> typing.Optional[typing.Tuple[str, str, str]]: netrc_info = netrc.netrc(str(netrc_path)) return netrc_info.authenticators(host) # type: ignore + + +def parse_header_links(value: str) -> typing.List[typing.Dict[str, str]]: + """ + Returns a list of parsed link headers, for more info see: + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link + The generic syntax of those is: + Link: < uri-reference >; param1=value1; param2="value2" + So for instance: + Link; '; type="image/jpeg",;' + would return + [ + {"url": "http:/.../front.jpeg", "type": "image/jpeg"}, + {"url": "http://.../back.jpeg"}, + ] + :param value: HTTP Link entity-header field + :return: list of parsed link headers + """ + links: typing.List[typing.Dict[str, str]] = [] + replace_chars = " '\"" + value = value.strip(replace_chars) + if not value: + return links + for val in re.split(", *<", value): + try: + url, params = val.split(";", 1) + except ValueError: + url, params = val, "" + link = {"url": url.strip("<> '\"")} + for param in params.split(";"): + try: + key, value = param.split("=") + except ValueError: + break + link[key.strip(replace_chars)] = value.strip(replace_chars) + links.append(link) + return links diff --git a/tests/models/test_responses.py b/tests/models/test_responses.py index a66daf5a..25a19353 100644 --- a/tests/models/test_responses.py +++ b/tests/models/test_responses.py @@ -290,6 +290,27 @@ def test_json_without_specified_encoding_decode_error(): response.json() +@pytest.mark.parametrize( + "headers, expected", + [ + ( + {"Link": "; rel='preload'"}, + {"preload": {"rel": "preload", "url": "https://example.com"}}, + ), + ( + {"Link": '; rel="hub", ; rel="self"'}, + { + "hub": {"url": "/hub", "rel": "hub"}, + "self": {"url": "/resource", "rel": "self"}, + }, + ), + ], +) +def test_link_headers(headers, expected): + response = httpx.Response(200, content=None, headers=headers) + assert response.links == expected + + @pytest.mark.asyncio async def test_stream_text(): async def iterator(): diff --git a/tests/test_utils.py b/tests/test_utils.py index 6cb4537e..2b2c7ab8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,7 +2,7 @@ import os import pytest -from httpx.utils import get_netrc_login, guess_json_utf +from httpx.utils import get_netrc_login, guess_json_utf, parse_header_links @pytest.mark.parametrize( @@ -64,3 +64,26 @@ def test_get_netrc_login(): None, "example-password", ) + + +@pytest.mark.parametrize( + "value, expected", + ( + ( + '; rel=front; type="image/jpeg"', + [{"url": "http:/.../front.jpeg", "rel": "front", "type": "image/jpeg"}], + ), + ("", [{"url": "http:/.../front.jpeg"}]), + (";", [{"url": "http:/.../front.jpeg"}]), + ( + '; type="image/jpeg",;', + [ + {"url": "http:/.../front.jpeg", "type": "image/jpeg"}, + {"url": "http://.../back.jpeg"}, + ], + ), + ("", []), + ), +) +def test_parse_header_links(value, expected): + assert parse_header_links(value) == expected