]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Added links property to BaseResponse (#211)
authoreuri10 <euri10@users.noreply.github.com>
Fri, 16 Aug 2019 14:07:52 +0000 (16:07 +0200)
committerSeth Michael Larson <sethmichaellarson@gmail.com>
Fri, 16 Aug 2019 14:07:52 +0000 (09:07 -0500)
* 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

httpx/models.py
httpx/utils.py
tests/models/test_responses.py
tests/test_utils.py

index 2ffc290e94ef6c23b372ae64a72fe06db9580247..e433320e7f883869c24ae0a8cd0fdfccb55235b9 100644 (file)
@@ -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"<Response [{self.status_code} {self.reason_phrase}]>"
 
index ff39935688637f44115866067d33a8ff7d5af252..6961326330a425953e858f8afe192e4aa6843e1a 100644 (file)
@@ -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; '<http:/.../front.jpeg>; type="image/jpeg",<http://.../back.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
index a66daf5a28ad11a0a56b8bda28e7b789b9be7cb3..25a19353df469b327baa32943f510f936aa78aeb 100644 (file)
@@ -290,6 +290,27 @@ def test_json_without_specified_encoding_decode_error():
             response.json()
 
 
+@pytest.mark.parametrize(
+    "headers, expected",
+    [
+        (
+            {"Link": "<https://example.com>; rel='preload'"},
+            {"preload": {"rel": "preload", "url": "https://example.com"}},
+        ),
+        (
+            {"Link": '</hub>; rel="hub", </resource>; 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():
index 6cb4537e3178dcd83356fbc413e3dfe861c92e66..2b2c7ab8298bbb4f203be125963b13e3053e3e4c 100644 (file)
@@ -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",
+    (
+        (
+            '<http:/.../front.jpeg>; rel=front; type="image/jpeg"',
+            [{"url": "http:/.../front.jpeg", "rel": "front", "type": "image/jpeg"}],
+        ),
+        ("<http:/.../front.jpeg>", [{"url": "http:/.../front.jpeg"}]),
+        ("<http:/.../front.jpeg>;", [{"url": "http:/.../front.jpeg"}]),
+        (
+            '<http:/.../front.jpeg>; type="image/jpeg",<http://.../back.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