From b78c83f185e2a4a722c23a1050c4bc760c9f1046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=B0=D1=8D=D1=82?= <85891169+novitae@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:44:29 +0200 Subject: [PATCH 1/4] Update session.py --- curl_cffi/requests/session.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/curl_cffi/requests/session.py b/curl_cffi/requests/session.py index ad2a96a..5abb704 100644 --- a/curl_cffi/requests/session.py +++ b/curl_cffi/requests/session.py @@ -21,6 +21,7 @@ TypedDict, Union, cast, + Type, ) from urllib.parse import ParseResult, parse_qsl, quote, unquote, urlencode, urljoin, urlparse @@ -86,6 +87,7 @@ class BaseSessionParams(TypedDict, total=False): debug: bool interface: Optional[str] cert: Optional[Union[str, Tuple[str, str]]] + response_factory: Optional[Type[Response]] else: ProxySpec = Dict[str, str] @@ -163,10 +165,10 @@ def _update_url_params(url: str, *params_list: Union[Dict, List, Tuple, None]) - return new_url -def _update_header_line(header_lines: List[str], key: str, value: str, force: bool = False): +def _update_header_line(header_lines: List[str], key: str, value: str): """Update header line list by key value pair.""" for idx, line in enumerate(header_lines): - if line.lower().startswith(key.lower() + ":") and force: + if line.lower().startswith(key.lower() + ":"): header_lines[idx] = f"{key}: {value}" break else: # if not break @@ -221,6 +223,7 @@ def __init__( debug: bool = False, interface: Optional[str] = None, cert: Optional[Union[str, Tuple[str, str]]] = None, + response_factory: Optional[Type[Response]] = None, ): self.headers = Headers(headers) self.cookies = Cookies(cookies) @@ -244,6 +247,9 @@ def __init__( self.debug = debug self.interface = interface self.cert = cert + assert response_factory is None or issubclass(response_factory, Response), \ + "The `response_factory` class must be a subclass of `curl_cffi.requests.models.Response`." + self.response_factory = response_factory if proxy and proxies: raise TypeError("Cannot specify both 'proxy' and 'proxies'") @@ -449,14 +455,14 @@ def _set_curl_options( # Add content-type if missing if json is not None: - _update_header_line(header_lines, "Content-Type", "application/json", force=True) + _update_header_line(header_lines, "Content-Type", "application/json") if isinstance(data, dict) and method != "POST": _update_header_line(header_lines, "Content-Type", "application/x-www-form-urlencoded") if isinstance(data, (str, bytes)): _update_header_line(header_lines, "Content-Type", "application/octet-stream") # Never send `Expect` header. - _update_header_line(header_lines, "Expect", "", force=True) + _update_header_line(header_lines, "Expect", "") c.setopt(CurlOpt.HTTPHEADER, [h.encode() for h in header_lines]) @@ -699,7 +705,7 @@ def qput(chunk): def _parse_response(self, curl, buffer, header_buffer, default_encoding): c = curl - rsp = Response(c) + rsp = (Response if self.response_factory is None else self.response_factory)(c) rsp.url = cast(bytes, c.getinfo(CurlInfo.EFFECTIVE_URL)).decode() if buffer: rsp.content = buffer.getvalue() From 447864059efce7bc9602744b54540a19dea5ad7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=B0=D1=8D=D1=82?= <85891169+novitae@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:56:55 +0200 Subject: [PATCH 2/4] Place back the force --- curl_cffi/requests/session.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/curl_cffi/requests/session.py b/curl_cffi/requests/session.py index 5abb704..e6d2c26 100644 --- a/curl_cffi/requests/session.py +++ b/curl_cffi/requests/session.py @@ -165,10 +165,10 @@ def _update_url_params(url: str, *params_list: Union[Dict, List, Tuple, None]) - return new_url -def _update_header_line(header_lines: List[str], key: str, value: str): +def _update_header_line(header_lines: List[str], key: str, value: str, force: bool = False): """Update header line list by key value pair.""" for idx, line in enumerate(header_lines): - if line.lower().startswith(key.lower() + ":"): + if line.lower().startswith(key.lower() + ":") and force: header_lines[idx] = f"{key}: {value}" break else: # if not break @@ -455,14 +455,14 @@ def _set_curl_options( # Add content-type if missing if json is not None: - _update_header_line(header_lines, "Content-Type", "application/json") + _update_header_line(header_lines, "Content-Type", "application/json", force=True) if isinstance(data, dict) and method != "POST": _update_header_line(header_lines, "Content-Type", "application/x-www-form-urlencoded") if isinstance(data, (str, bytes)): _update_header_line(header_lines, "Content-Type", "application/octet-stream") # Never send `Expect` header. - _update_header_line(header_lines, "Expect", "") + _update_header_line(header_lines, "Expect", "", force=True) c.setopt(CurlOpt.HTTPHEADER, [h.encode() for h in header_lines]) From 303da691f03325a847e6d3aaa8b0c5da74fbce51 Mon Sep 17 00:00:00 2001 From: ae <85891169+novitae@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:32:13 +0200 Subject: [PATCH 3/4] response_factory -> response_class + examples + tests --- curl_cffi/requests/session.py | 16 ++++++++------ examples/custom_response_class.py | 14 ++++++++++++ tests/integration/test_response_class.py | 27 ++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 examples/custom_response_class.py create mode 100644 tests/integration/test_response_class.py diff --git a/curl_cffi/requests/session.py b/curl_cffi/requests/session.py index e6d2c26..39b70bd 100644 --- a/curl_cffi/requests/session.py +++ b/curl_cffi/requests/session.py @@ -87,7 +87,7 @@ class BaseSessionParams(TypedDict, total=False): debug: bool interface: Optional[str] cert: Optional[Union[str, Tuple[str, str]]] - response_factory: Optional[Type[Response]] + response_class: Optional[Type[Response]] else: ProxySpec = Dict[str, str] @@ -223,7 +223,7 @@ def __init__( debug: bool = False, interface: Optional[str] = None, cert: Optional[Union[str, Tuple[str, str]]] = None, - response_factory: Optional[Type[Response]] = None, + response_class: Optional[Type[Response]] = None, ): self.headers = Headers(headers) self.cookies = Cookies(cookies) @@ -247,9 +247,13 @@ def __init__( self.debug = debug self.interface = interface self.cert = cert - assert response_factory is None or issubclass(response_factory, Response), \ - "The `response_factory` class must be a subclass of `curl_cffi.requests.models.Response`." - self.response_factory = response_factory + + if response_class is None: + response_class = Response + elif issubclass(response_class, Response) is False: + raise TypeError( "`response_class` must be a subclass of `curl_cffi.requests.models.Response`" + f" not of type `{response_class}`" ) + self.response_class = response_class if proxy and proxies: raise TypeError("Cannot specify both 'proxy' and 'proxies'") @@ -705,7 +709,7 @@ def qput(chunk): def _parse_response(self, curl, buffer, header_buffer, default_encoding): c = curl - rsp = (Response if self.response_factory is None else self.response_factory)(c) + rsp = self.response_class(c) rsp.url = cast(bytes, c.getinfo(CurlInfo.EFFECTIVE_URL)).decode() if buffer: rsp.content = buffer.getvalue() diff --git a/examples/custom_response_class.py b/examples/custom_response_class.py new file mode 100644 index 0000000..5544c3f --- /dev/null +++ b/examples/custom_response_class.py @@ -0,0 +1,14 @@ +from curl_cffi import requests + +class CustomResponse(requests.Response): + @property + def status(self): + return self.status_code + + def custom_method(): + return "this is a custom method" + +session = requests.Session(response_class=CustomResponse) +response: CustomResponse = session.get("http://example.com") +print(response.status) +print(response.custom_method()) diff --git a/tests/integration/test_response_class.py b/tests/integration/test_response_class.py new file mode 100644 index 0000000..a71efa9 --- /dev/null +++ b/tests/integration/test_response_class.py @@ -0,0 +1,27 @@ +from curl_cffi import requests + +def test_default_response(): + response = requests.get("http://example.com") + assert type(response) == requests.Response + print(response.status_code) + +class CustomResponse(requests.Response): + @property + def status(self): + return self.status_code + +def test_custom_response(): + session = requests.Session(response_class=CustomResponse) + response = session.get("http://example.com") + assert isinstance(response, CustomResponse) + assert hasattr(response, "status") + print(response.status) + +class WrongTypeResponse: pass + +def test_wrong_type_custom_response(): + try: + requests.Session(response_class=WrongTypeResponse) + assert False, "session was created without raising issue for wrong response class type" + except TypeError: + print("Wrong response class type detected") From 81cf97f13f1335a1d2f720254bb2b2f689764ab2 Mon Sep 17 00:00:00 2001 From: ae <85891169+novitae@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:55:23 +0200 Subject: [PATCH 4/4] changes --- curl_cffi/requests/session.py | 2 +- examples/custom_response_class.py | 13 +++++++++++-- tests/integration/test_response_class.py | 8 +++----- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/curl_cffi/requests/session.py b/curl_cffi/requests/session.py index 39b70bd..b4aac77 100644 --- a/curl_cffi/requests/session.py +++ b/curl_cffi/requests/session.py @@ -250,7 +250,7 @@ def __init__( if response_class is None: response_class = Response - elif issubclass(response_class, Response) is False: + elif not issubclass(response_class, Response): raise TypeError( "`response_class` must be a subclass of `curl_cffi.requests.models.Response`" f" not of type `{response_class}`" ) self.response_class = response_class diff --git a/examples/custom_response_class.py b/examples/custom_response_class.py index 5544c3f..f367943 100644 --- a/examples/custom_response_class.py +++ b/examples/custom_response_class.py @@ -1,14 +1,23 @@ from curl_cffi import requests +from curl_cffi.curl import Curl, CurlInfo +from typing import cast class CustomResponse(requests.Response): + def __init__(self, curl: Curl | None = None, request: requests.Request | None = None): + super().__init__(curl, request) + self.local_port = cast(int, curl.getinfo(CurlInfo.LOCAL_PORT)) + self.connect_time = cast(float, curl.getinfo(CurlInfo.CONNECT_TIME)) + @property def status(self): return self.status_code - def custom_method(): + def custom_method(self): return "this is a custom method" session = requests.Session(response_class=CustomResponse) response: CustomResponse = session.get("http://example.com") -print(response.status) +print(f"{response.status=}") print(response.custom_method()) +print(f"{response.local_port=}") +print(f"{response.connect_time=}") \ No newline at end of file diff --git a/tests/integration/test_response_class.py b/tests/integration/test_response_class.py index a71efa9..985e951 100644 --- a/tests/integration/test_response_class.py +++ b/tests/integration/test_response_class.py @@ -1,3 +1,4 @@ +import pytest from curl_cffi import requests def test_default_response(): @@ -20,8 +21,5 @@ def test_custom_response(): class WrongTypeResponse: pass def test_wrong_type_custom_response(): - try: - requests.Session(response_class=WrongTypeResponse) - assert False, "session was created without raising issue for wrong response class type" - except TypeError: - print("Wrong response class type detected") + with pytest.raises(TypeError): + requests.Session(response_class=WrongTypeResponse) \ No newline at end of file