src.lib.http_client

HTTP client interface for dependency injection.

  1"""HTTP client interface for dependency injection."""
  2# BK: Ignored, because this is how "requests" works, and wrapping it to avoid
  3# typing issues adds far too much complexity.
  4# pyright: reportExplicitAny=false, reportAny=false
  5
  6from typing import Any, Protocol, TypeAlias
  7
  8import requests
  9
 10
 11# any object that can be serialized to JSON. Used exactly like this in requests
 12JSON: TypeAlias = Any
 13
 14
 15class HttpResponse(Protocol):
 16    """Protocol defining the HTTP response interface.
 17
 18    This protocol allows type-safe access to response objects from both
 19    real HTTP clients and test doubles.
 20    """
 21
 22    status_code: int
 23    url: str
 24
 25    @property
 26    def json(self) -> JSON:
 27        """Return the JSON response content."""
 28        ...
 29
 30    @property
 31    def content(self) -> bytes:
 32        """Return the raw response content."""
 33        ...
 34
 35    @property
 36    def text(self) -> str:
 37        """Return the response content as a string."""
 38        ...
 39
 40
 41class HttpClient(Protocol):
 42    """Protocol defining the HTTP client interface.
 43
 44    This protocol allows dependency injection of HTTP clients for testing
 45    while maintaining type safety and following the project's architectural patterns.
 46
 47    The protocol is designed to be compatible with both the requests module
 48    and test doubles that implement the same interface.
 49    """
 50
 51    def get(self, url: str) -> HttpResponse:
 52        """Send a GET request.
 53
 54        Args:
 55            url: The URL to request.
 56            **kwargs: Additional request parameters.
 57
 58        Returns:
 59            The response object.
 60        """
 61        ...
 62
 63    def post(self, url: str, json: JSON) -> HttpResponse:
 64        """Send a POST request.
 65
 66        Args:
 67            url: The URL to request.
 68            **kwargs: Additional request parameters.
 69
 70        Returns:
 71            The response object.
 72        """
 73        ...
 74
 75    def put(self, url: str, json: JSON) -> HttpResponse:
 76        """Send a PUT request.
 77
 78        Args:
 79            url: The URL to request.
 80            **kwargs: Additional request parameters.
 81
 82        Returns:
 83            The response object.
 84        """
 85        ...
 86
 87    def delete(self, url: str, json: JSON | None = None) -> HttpResponse:
 88        """Send a DELETE request.
 89
 90        Args:
 91            url: The URL to request.
 92            **kwargs: Additional request parameters.
 93
 94        Returns:
 95            The response object.
 96        """
 97        ...
 98
 99
100class RequestsHttpClient:
101    """Adapter that makes the requests module compatible with HttpClient protocol.
102
103    This is a simple wrapper that delegates to the requests module functions.
104    While the requests module itself could potentially be used directly,
105    this wrapper ensures explicit compatibility with the HttpClient protocol.
106    """
107
108    _default_timeout: int = 10
109
110    def get(self, url: str) -> HttpResponse:
111        return self._request("GET", url)
112
113    def post(self, url: str, json: JSON) -> HttpResponse:
114        return self._request("POST", url, json=json)
115
116    def put(self, url: str, json: JSON) -> HttpResponse:
117        return self._request("PUT", url, json=json)
118
119    def delete(self, url: str, json: JSON | None = None) -> HttpResponse:
120        return self._request("DELETE", url, json=json)
121
122    def _request(self, method: str, url: str, json: JSON | None = None) -> HttpResponse:
123        return requests.request(
124            method=method, url=url, json=json, timeout=self._default_timeout
125        )
JSON: TypeAlias = Any
class HttpResponse(typing.Protocol):
16class HttpResponse(Protocol):
17    """Protocol defining the HTTP response interface.
18
19    This protocol allows type-safe access to response objects from both
20    real HTTP clients and test doubles.
21    """
22
23    status_code: int
24    url: str
25
26    @property
27    def json(self) -> JSON:
28        """Return the JSON response content."""
29        ...
30
31    @property
32    def content(self) -> bytes:
33        """Return the raw response content."""
34        ...
35
36    @property
37    def text(self) -> str:
38        """Return the response content as a string."""
39        ...

Protocol defining the HTTP response interface.

This protocol allows type-safe access to response objects from both real HTTP clients and test doubles.

HttpResponse(*args, **kwargs)
1866def _no_init_or_replace_init(self, *args, **kwargs):
1867    cls = type(self)
1868
1869    if cls._is_protocol:
1870        raise TypeError('Protocols cannot be instantiated')
1871
1872    # Already using a custom `__init__`. No need to calculate correct
1873    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1874    if cls.__init__ is not _no_init_or_replace_init:
1875        return
1876
1877    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1878    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1879    # searches for a proper new `__init__` in the MRO. The new `__init__`
1880    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1881    # instantiation of the protocol subclass will thus use the new
1882    # `__init__` and no longer call `_no_init_or_replace_init`.
1883    for base in cls.__mro__:
1884        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1885        if init is not _no_init_or_replace_init:
1886            cls.__init__ = init
1887            break
1888    else:
1889        # should not happen
1890        cls.__init__ = object.__init__
1891
1892    cls.__init__(self, *args, **kwargs)
status_code: int
url: str
json: Any
26    @property
27    def json(self) -> JSON:
28        """Return the JSON response content."""
29        ...

Return the JSON response content.

content: bytes
31    @property
32    def content(self) -> bytes:
33        """Return the raw response content."""
34        ...

Return the raw response content.

text: str
36    @property
37    def text(self) -> str:
38        """Return the response content as a string."""
39        ...

Return the response content as a string.

class HttpClient(typing.Protocol):
42class HttpClient(Protocol):
43    """Protocol defining the HTTP client interface.
44
45    This protocol allows dependency injection of HTTP clients for testing
46    while maintaining type safety and following the project's architectural patterns.
47
48    The protocol is designed to be compatible with both the requests module
49    and test doubles that implement the same interface.
50    """
51
52    def get(self, url: str) -> HttpResponse:
53        """Send a GET request.
54
55        Args:
56            url: The URL to request.
57            **kwargs: Additional request parameters.
58
59        Returns:
60            The response object.
61        """
62        ...
63
64    def post(self, url: str, json: JSON) -> HttpResponse:
65        """Send a POST request.
66
67        Args:
68            url: The URL to request.
69            **kwargs: Additional request parameters.
70
71        Returns:
72            The response object.
73        """
74        ...
75
76    def put(self, url: str, json: JSON) -> HttpResponse:
77        """Send a PUT request.
78
79        Args:
80            url: The URL to request.
81            **kwargs: Additional request parameters.
82
83        Returns:
84            The response object.
85        """
86        ...
87
88    def delete(self, url: str, json: JSON | None = None) -> HttpResponse:
89        """Send a DELETE request.
90
91        Args:
92            url: The URL to request.
93            **kwargs: Additional request parameters.
94
95        Returns:
96            The response object.
97        """
98        ...

Protocol defining the HTTP client interface.

This protocol allows dependency injection of HTTP clients for testing while maintaining type safety and following the project's architectural patterns.

The protocol is designed to be compatible with both the requests module and test doubles that implement the same interface.

HttpClient(*args, **kwargs)
1866def _no_init_or_replace_init(self, *args, **kwargs):
1867    cls = type(self)
1868
1869    if cls._is_protocol:
1870        raise TypeError('Protocols cannot be instantiated')
1871
1872    # Already using a custom `__init__`. No need to calculate correct
1873    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1874    if cls.__init__ is not _no_init_or_replace_init:
1875        return
1876
1877    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1878    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1879    # searches for a proper new `__init__` in the MRO. The new `__init__`
1880    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1881    # instantiation of the protocol subclass will thus use the new
1882    # `__init__` and no longer call `_no_init_or_replace_init`.
1883    for base in cls.__mro__:
1884        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1885        if init is not _no_init_or_replace_init:
1886            cls.__init__ = init
1887            break
1888    else:
1889        # should not happen
1890        cls.__init__ = object.__init__
1891
1892    cls.__init__(self, *args, **kwargs)
def get(self, url: str) -> HttpResponse:
52    def get(self, url: str) -> HttpResponse:
53        """Send a GET request.
54
55        Args:
56            url: The URL to request.
57            **kwargs: Additional request parameters.
58
59        Returns:
60            The response object.
61        """
62        ...

Send a GET request.

Args: url: The URL to request. **kwargs: Additional request parameters.

Returns: The response object.

def post(self, url: str, json: Any) -> HttpResponse:
64    def post(self, url: str, json: JSON) -> HttpResponse:
65        """Send a POST request.
66
67        Args:
68            url: The URL to request.
69            **kwargs: Additional request parameters.
70
71        Returns:
72            The response object.
73        """
74        ...

Send a POST request.

Args: url: The URL to request. **kwargs: Additional request parameters.

Returns: The response object.

def put(self, url: str, json: Any) -> HttpResponse:
76    def put(self, url: str, json: JSON) -> HttpResponse:
77        """Send a PUT request.
78
79        Args:
80            url: The URL to request.
81            **kwargs: Additional request parameters.
82
83        Returns:
84            The response object.
85        """
86        ...

Send a PUT request.

Args: url: The URL to request. **kwargs: Additional request parameters.

Returns: The response object.

def delete( self, url: str, json: Any | None = None) -> HttpResponse:
88    def delete(self, url: str, json: JSON | None = None) -> HttpResponse:
89        """Send a DELETE request.
90
91        Args:
92            url: The URL to request.
93            **kwargs: Additional request parameters.
94
95        Returns:
96            The response object.
97        """
98        ...

Send a DELETE request.

Args: url: The URL to request. **kwargs: Additional request parameters.

Returns: The response object.

class RequestsHttpClient:
101class RequestsHttpClient:
102    """Adapter that makes the requests module compatible with HttpClient protocol.
103
104    This is a simple wrapper that delegates to the requests module functions.
105    While the requests module itself could potentially be used directly,
106    this wrapper ensures explicit compatibility with the HttpClient protocol.
107    """
108
109    _default_timeout: int = 10
110
111    def get(self, url: str) -> HttpResponse:
112        return self._request("GET", url)
113
114    def post(self, url: str, json: JSON) -> HttpResponse:
115        return self._request("POST", url, json=json)
116
117    def put(self, url: str, json: JSON) -> HttpResponse:
118        return self._request("PUT", url, json=json)
119
120    def delete(self, url: str, json: JSON | None = None) -> HttpResponse:
121        return self._request("DELETE", url, json=json)
122
123    def _request(self, method: str, url: str, json: JSON | None = None) -> HttpResponse:
124        return requests.request(
125            method=method, url=url, json=json, timeout=self._default_timeout
126        )

Adapter that makes the requests module compatible with HttpClient protocol.

This is a simple wrapper that delegates to the requests module functions. While the requests module itself could potentially be used directly, this wrapper ensures explicit compatibility with the HttpClient protocol.

def get(self, url: str) -> HttpResponse:
111    def get(self, url: str) -> HttpResponse:
112        return self._request("GET", url)
def post(self, url: str, json: Any) -> HttpResponse:
114    def post(self, url: str, json: JSON) -> HttpResponse:
115        return self._request("POST", url, json=json)
def put(self, url: str, json: Any) -> HttpResponse:
117    def put(self, url: str, json: JSON) -> HttpResponse:
118        return self._request("PUT", url, json=json)
def delete( self, url: str, json: Any | None = None) -> HttpResponse:
120    def delete(self, url: str, json: JSON | None = None) -> HttpResponse:
121        return self._request("DELETE", url, json=json)