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 )
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.
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)
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.
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)
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.
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.
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.
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.
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.