redc 0.1.0.dev5__tar.gz → 0.1.1.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. redc-0.1.1.dev1/.github/workflows/before-all.sh +30 -0
  2. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/CMakeLists.txt +1 -1
  3. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/PKG-INFO +6 -4
  4. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/README.md +5 -3
  5. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/pyproject.toml +4 -3
  6. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/__init__.py +3 -2
  7. redc-0.1.1.dev1/redc/callbacks.py +81 -0
  8. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/client.py +147 -71
  9. redc-0.1.1.dev1/redc/exceptions/__init__.py +22 -0
  10. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/ext/redc.cpp +67 -27
  11. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/ext/redc.h +25 -15
  12. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/response.py +6 -11
  13. redc-0.1.1.dev1/wheelhouse/redc-0.1.1.dev1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +0 -0
  14. redc-0.1.0.dev5/wheelhouse/redc-0.1.0.dev5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl → redc-0.1.1.dev1/wheelhouse/redc-0.1.1.dev1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +0 -0
  15. redc-0.1.0.dev5/wheelhouse/redc-0.1.0.dev5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl → redc-0.1.1.dev1/wheelhouse/redc-0.1.1.dev1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +0 -0
  16. redc-0.1.0.dev5/wheelhouse/redc-0.1.0.dev5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl → redc-0.1.1.dev1/wheelhouse/redc-0.1.1.dev1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +0 -0
  17. redc-0.1.0.dev5/wheelhouse/redc-0.1.0.dev5-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl → redc-0.1.1.dev1/wheelhouse/redc-0.1.1.dev1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +0 -0
  18. redc-0.1.0.dev5/redc/callbacks.py +0 -40
  19. redc-0.1.0.dev5/redc/exceptions/__init__.py +0 -2
  20. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/.clang-format +0 -0
  21. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/.github/workflows/build_wheels.yml +0 -0
  22. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/.gitignore +0 -0
  23. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/CMake/PreventInSourceBuild.cmake +0 -0
  24. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/LICENSE +0 -0
  25. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/assets/images/redc-logo.png +0 -0
  26. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/callback.py +0 -0
  27. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/codes.py +0 -0
  28. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/ext/utils/concurrentqueue.h +0 -0
  29. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/ext/utils/curl_utils.h +0 -0
  30. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/utils/__init__.py +0 -0
  31. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/utils/headers.py +0 -0
  32. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/utils/http.py +0 -0
  33. {redc-0.1.0.dev5 → redc-0.1.1.dev1}/redc/utils/json_encoder.py +0 -0
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bash
2
+
3
+ CURL_VERSION="8.11.1"
4
+
5
+ # deps
6
+ yum install wget gcc make libpsl-devel libidn-devel zlib-devel libnghttp2-devel perl-IPC-Cmd -y
7
+
8
+ # openssl from source
9
+ git clone --depth 1 https://github.com/openssl/openssl
10
+ cd openssl
11
+ ./Configure
12
+ make -j100
13
+ make install
14
+ ldconfig
15
+ cd .. && rm -rf openssl
16
+
17
+ # curl from source
18
+ wget https://curl.se/download/curl-$CURL_VERSION.tar.gz
19
+ tar -xzvf curl-$CURL_VERSION.tar.gz
20
+ rm curl-$CURL_VERSION.tar.gz
21
+
22
+ cd curl-$CURL_VERSION
23
+ ./configure --with-openssl --enable-cookies --with-zlib --enable-threaded-resolver --enable-ipv6 --enable-proxy --with-ca-fallback --with-ca-bundle=/etc/ssl/certs/ca-certificates.crt
24
+ make -j100
25
+ make install
26
+ ldconfig
27
+ curl --version
28
+
29
+ cd ..
30
+ rm -rf curl-$CURL_VERSION
@@ -43,7 +43,7 @@ if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
43
43
  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
44
44
  endif()
45
45
 
46
- nanobind_add_module(redc_ext STABLE_ABI LTO redc/ext/redc.cpp)
46
+ nanobind_add_module(redc_ext STABLE_ABI FREE_THREADED LTO redc/ext/redc.cpp)
47
47
 
48
48
  target_link_libraries(redc_ext PRIVATE CURL::libcurl)
49
49
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: redc
3
- Version: 0.1.0.dev5
3
+ Version: 0.1.1.dev1
4
4
  Summary: RedC is a high-performance, asynchronous HTTP client library for Python, built on top of the powerful curl library
5
5
  Keywords: asyncio,http,client,http-client,curl,libcurl
6
6
  Author-Email: AYMEN Mohammed <let.me.code.safe@gmail.com>
@@ -11,10 +11,10 @@ Requires-Python: >=3.9
11
11
  Description-Content-Type: text/markdown
12
12
 
13
13
  <div align="center">
14
- <img src="/assets/images/redc-logo.png">
14
+ <img src="https://raw.githubusercontent.com/AYMENJD/redc/refs/heads/main/assets/images/redc-logo.png">
15
15
  </div>
16
16
 
17
- [![Version](https://img.shields.io/pypi/v/redc?style=flat&logo=curl&logoColor=red&color=red)](https://pypi.org/project/RedC) [![Downloads](https://static.pepy.tech/personalized-badge/redc?period=month&units=none&left_color=grey&right_color=brightgreen&left_text=Downloads)](https://pepy.tech/project/redc)
17
+ [![Version](https://img.shields.io/pypi/v/redc?style=flat&logo=curl&logoColor=red&color=red)](https://pypi.org/project/RedC) [![CURL version](https://img.shields.io/badge/Curl-v8.11.1-red?logo=curl)](https://curl.se/ch/8.11.1.html) [![Downloads](https://static.pepy.tech/personalized-badge/redc?period=month&units=none&left_color=grey&right_color=brightgreen&left_text=Downloads)](https://pepy.tech/project/redc)
18
18
 
19
19
  **RedC** is a **high-performance**, asynchronous **HTTP** client library for **Python**, built on top of the powerful **curl** library. It provides a simple and intuitive interface for making HTTP requests and handling responses
20
20
 
@@ -43,6 +43,7 @@ async def main():
43
43
  async with Client(base_url="https://jsonplaceholder.typicode.com") as client:
44
44
  # Make a GET request
45
45
  response = await client.get("/posts/1")
46
+ response.raise_for_status()
46
47
  print(response.status_code) # 200
47
48
  print(response.json()) # {'userId': 1, 'id': 1, 'title': '...', 'body': '...'}
48
49
 
@@ -51,6 +52,7 @@ async def main():
51
52
  "/posts",
52
53
  json={"title": "foo", "body": "bar", "userId": 1},
53
54
  )
55
+ response.raise_for_status()
54
56
  print(response.status_code) # 201
55
57
  print(response.json()) # {'id': 101, ...}
56
58
 
@@ -59,4 +61,4 @@ asyncio.run(main())
59
61
 
60
62
  ## License
61
63
 
62
- MIT [LICENSE](LICENSE)
64
+ MIT [LICENSE](https://github.com/AYMENJD/redc/blob/main/LICENSE)
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
- <img src="/assets/images/redc-logo.png">
2
+ <img src="https://raw.githubusercontent.com/AYMENJD/redc/refs/heads/main/assets/images/redc-logo.png">
3
3
  </div>
4
4
 
5
- [![Version](https://img.shields.io/pypi/v/redc?style=flat&logo=curl&logoColor=red&color=red)](https://pypi.org/project/RedC) [![Downloads](https://static.pepy.tech/personalized-badge/redc?period=month&units=none&left_color=grey&right_color=brightgreen&left_text=Downloads)](https://pepy.tech/project/redc)
5
+ [![Version](https://img.shields.io/pypi/v/redc?style=flat&logo=curl&logoColor=red&color=red)](https://pypi.org/project/RedC) [![CURL version](https://img.shields.io/badge/Curl-v8.11.1-red?logo=curl)](https://curl.se/ch/8.11.1.html) [![Downloads](https://static.pepy.tech/personalized-badge/redc?period=month&units=none&left_color=grey&right_color=brightgreen&left_text=Downloads)](https://pepy.tech/project/redc)
6
6
 
7
7
  **RedC** is a **high-performance**, asynchronous **HTTP** client library for **Python**, built on top of the powerful **curl** library. It provides a simple and intuitive interface for making HTTP requests and handling responses
8
8
 
@@ -31,6 +31,7 @@ async def main():
31
31
  async with Client(base_url="https://jsonplaceholder.typicode.com") as client:
32
32
  # Make a GET request
33
33
  response = await client.get("/posts/1")
34
+ response.raise_for_status()
34
35
  print(response.status_code) # 200
35
36
  print(response.json()) # {'userId': 1, 'id': 1, 'title': '...', 'body': '...'}
36
37
 
@@ -39,6 +40,7 @@ async def main():
39
40
  "/posts",
40
41
  json={"title": "foo", "body": "bar", "userId": 1},
41
42
  )
43
+ response.raise_for_status()
42
44
  print(response.status_code) # 201
43
45
  print(response.json()) # {'id': 101, ...}
44
46
 
@@ -47,4 +49,4 @@ asyncio.run(main())
47
49
 
48
50
  ## License
49
51
 
50
- MIT [LICENSE](LICENSE)
52
+ MIT [LICENSE](https://github.com/AYMENJD/redc/blob/main/LICENSE)
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["scikit-build-core >=0.10", "nanobind >=1.3.2"]
2
+ requires = ["scikit-build-core >=0.10", "nanobind >=2.2.0"]
3
3
  build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "redc"
7
- version = "0.1.0.dev5"
7
+ version = "0.1.1.dev1"
8
8
  description = "RedC is a high-performance, asynchronous HTTP client library for Python, built on top of the powerful curl library"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "AYMEN Mohammed", email = "let.me.code.safe@gmail.com" }]
@@ -23,9 +23,10 @@ wheel.py-api = "cp312"
23
23
  [tool.cibuildwheel]
24
24
  build-verbosity = 1
25
25
  build = "cp39* cp310* cp311* cp312* cp313*"
26
+ free-threaded-support = true
26
27
  skip = "*musllinux*"
27
28
  archs = ["x86_64"]
28
29
 
29
30
  [tool.cibuildwheel.linux]
30
- before-all = "yum install wget gcc openssl-devel make libpsl-devel -y && wget https://curl.se/download/curl-8.11.1.tar.gz && tar -xzvf curl-8.11.1.tar.gz && rm curl-8.11.1.tar.gz && cd curl-8.11.1 && ./configure --with-openssl --with-ca-fallback --with-ca-bundle=/etc/ssl/certs/ca-certificates.crt && make -j && make install && ldconfig && curl --version && cd .. && rm -rf curl-8.11.1"
31
+ before-all = ".github/workflows/before-all.sh"
31
32
  manylinux-x86_64-image = "manylinux2014"
@@ -1,4 +1,4 @@
1
- from .callbacks import StreamCallback
1
+ from .callbacks import StreamCallback, ProgressCallback
2
2
  from .client import Client
3
3
  from .codes import HTTPStatus
4
4
  from .exceptions import HTTPError
@@ -11,10 +11,11 @@ __all__ = [
11
11
  "HTTPError",
12
12
  "HTTPStatus",
13
13
  "StreamCallback",
14
+ "ProgressCallback",
14
15
  "utils",
15
16
  ]
16
17
 
17
- __version__ = "0.1.0.dev5"
18
+ __version__ = "0.1.1.dev1"
18
19
  __copyright__ = "Copyright (c) 2025 RedC, AYMENJD"
19
20
  __license__ = "MIT License"
20
21
 
@@ -0,0 +1,81 @@
1
+ import inspect
2
+ from typing import Callable
3
+
4
+
5
+ class StreamCallback:
6
+ """A class for creating a stream callback"""
7
+
8
+ def __init__(self, callback: Callable[[bytes, int], None]):
9
+ """A callback handler for streaming data
10
+
11
+ Example:
12
+ .. code-block:: python
13
+
14
+ >>> def callback(data: bytes, data_size: int):
15
+ ... print(f"Received {len(data)}")
16
+ >>> stream_callback = StreamCallback(callback)
17
+ >>> client.get("https://example.com/", stream_callback=stream_callback)
18
+
19
+ Parameters:
20
+ callback (``Callable[[bytes, int], None]``):
21
+ A function that accepts two arguments: data (``bytes``) and data_size (``int``)
22
+ The function cannot be asynchronous
23
+ """
24
+
25
+ self.callback = callback
26
+ self._validate_callback()
27
+
28
+ def _validate_callback(self):
29
+ if inspect.iscoroutinefunction(self.callback):
30
+ raise TypeError("Callback function cannot be asynchronous")
31
+
32
+ signature = inspect.signature(self.callback)
33
+
34
+ parameters = signature.parameters
35
+ num_parameters = len(parameters)
36
+
37
+ if num_parameters != 2:
38
+ raise TypeError(
39
+ f"Callback function must accept two arguments only callback(data: bytes, data_size: int) but it accepts {num_parameters}."
40
+ )
41
+
42
+
43
+ class ProgressCallback:
44
+ """A class for creating a progress callback"""
45
+
46
+ def __init__(self, callback: Callable[[int, int, int, int], None]):
47
+ """A callback handler for progress updates
48
+
49
+ Example:
50
+ .. code-block:: python
51
+
52
+ >>> def callback(dltotal: int, dlnow: int, ultotal: int, ulnow: int):
53
+ ... print(f"Downloaded {dlnow}/{dltotal}, Uploaded {ulnow}/{ultotal}")
54
+ >>> progress_callback = ProgressCallback(callback)
55
+ >>> client.get("https://example.com/", progress_callback=progress_callback)
56
+
57
+ Parameters:
58
+ callback (``Callable[[int, int, int, int], None]``):
59
+ A function that accepts four arguments:
60
+ - dltotal (``int``): Total bytes expected to be downloaded
61
+ - dlnow (``int``): Bytes downloaded so far
62
+ - ultotal (``int``): Total bytes expected to be uploaded
63
+ - ulnow (``int``): Bytes uploaded so far
64
+ The function cannot be asynchronous.
65
+ """
66
+
67
+ self.callback = callback
68
+ self._validate_callback()
69
+
70
+ def _validate_callback(self):
71
+ if inspect.iscoroutinefunction(self.callback):
72
+ raise TypeError("Callback function cannot be asynchronous")
73
+
74
+ signature = inspect.signature(self.callback)
75
+ parameters = signature.parameters
76
+ num_parameters = len(parameters)
77
+
78
+ if num_parameters != 4:
79
+ raise TypeError(
80
+ f"Callback function must accept exactly four arguments (dltotal: int, dlnow: int, ultotal: int, ulnow: int) but it accepts {num_parameters}."
81
+ )
@@ -1,9 +1,12 @@
1
1
  from urllib.parse import urlencode
2
2
 
3
- from .callbacks import StreamCallback
3
+ from .callbacks import StreamCallback, ProgressCallback
4
4
  from .redc_ext import RedC
5
5
  from .response import Response
6
- from .utils import json_dumps, parse_base_url
6
+ from .utils import json_dumps, parse_base_url, Headers
7
+
8
+ import asyncio
9
+ import redc
7
10
 
8
11
 
9
12
  class Client:
@@ -13,7 +16,11 @@ class Client:
13
16
  self,
14
17
  base_url: str = None,
15
18
  buffer_size: int = 16384,
19
+ headers: dict = None,
20
+ timeout: tuple = (30.0, 0.0),
21
+ ca_cert_path: str = None,
16
22
  force_verbose: bool = None,
23
+ raise_for_status: bool = False,
17
24
  json_encoder=json_dumps,
18
25
  ):
19
26
  """
@@ -32,35 +39,78 @@ class Client:
32
39
  buffer_size (``int``, *optional*):
33
40
  The buffer size for libcurl. Must be greater than ``1024`` bytes. Default is ``16384`` (16KB)
34
41
 
42
+ headers (``dict``, *optional*):
43
+ Headers to include in every request. Default is ``None``
44
+
45
+ timeout (``tuple``, *optional*):
46
+ A tuple of `(total_timeout, connect_timeout)` in seconds to include in every request. Default is ``(30.0, 0.0)``
47
+
48
+ ca_cert_path (``str``, *optional*):
49
+ Path to a CA certificate bundle file for SSL/TLS verification. Default is ``None``
50
+
35
51
  force_verbose (``bool``, *optional*):
36
52
  Force verbose output for all requests. Default is ``None``
37
53
 
54
+ raise_for_status (``bool``, *optional*):
55
+ If ``True``, automatically raises an :class:`redc.HTTPError` for responses with HTTP status codes
56
+ indicating an error (i.e., 4xx or 5xx) or for CURL errors (e.g., network issues, timeouts). Default is ``False``
57
+
38
58
  json_encoder (``Callable`` , *optional*):
39
- A callable for encoding JSON data. Default is ``json_dumps``
59
+ A callable for encoding JSON data. Default is :class:`redc.utils.json_dumps`
40
60
  """
41
61
 
42
62
  assert isinstance(base_url, (str, type(None))), "base_url must be string"
43
63
  assert isinstance(buffer_size, int), "buffer_size must be int"
44
- assert buffer_size >= 1024, "buffer_size must be bigger than 1024 bytes"
45
-
64
+ assert isinstance(ca_cert_path, (str, type(None))), (
65
+ "ca_cert_path must be string"
66
+ )
67
+ assert isinstance(timeout, tuple) and len(timeout) == 2, (
68
+ "timeout must be a tuple of (total_timeout, connect_timeout)"
69
+ )
46
70
  assert isinstance(force_verbose, (bool, type(None))), (
47
71
  "force_verbose must be bool or None"
48
72
  )
73
+ assert isinstance(raise_for_status, bool), "raise_for_status must be bool"
74
+
75
+ assert buffer_size >= 1024, "buffer_size must be bigger than 1024 bytes"
49
76
 
50
77
  self.force_verbose = force_verbose
78
+ self.raise_for_status = raise_for_status
51
79
 
52
80
  self.__base_url = (
53
- None if not isinstance(base_url, str) else parse_base_url(base_url)
81
+ parse_base_url(base_url) if isinstance(base_url, str) else None
54
82
  )
83
+ self.__default_headers = Headers(headers if isinstance(headers, dict) else {})
84
+ self.__timeout = timeout
85
+ self.__ca_cert_path = ca_cert_path if isinstance(ca_cert_path, str) else ""
55
86
  self.__json_encoder = json_encoder
87
+ self.__loop = asyncio.get_event_loop()
56
88
  self.__redc_ext = RedC(buffer_size)
57
89
 
90
+ self.__set_default_headers()
91
+
58
92
  async def __aenter__(self):
59
93
  return self
60
94
 
61
95
  async def __aexit__(self, exc_type, exc_val, exc_tb):
62
96
  await self.close()
63
97
 
98
+ @property
99
+ def is_running(self):
100
+ """Checks if RedC is currently running
101
+
102
+ Returns:
103
+ ``bool``: ``True`` if RedC is running, False otherwise
104
+ """
105
+
106
+ return self.__redc_ext.is_running()
107
+
108
+ @property
109
+ def default_headers(self):
110
+ """Returns default headers that are set on all requests"""
111
+
112
+ return self.__default_headers
113
+
64
114
  async def request(
65
115
  self,
66
116
  method: str,
@@ -70,12 +120,12 @@ class Client:
70
120
  data: dict[str, str] = None,
71
121
  files: dict[str, str] = None,
72
122
  headers: dict[str, str] = None,
73
- timeout: float = 30.0,
74
- connect_timeout: float = 0.0,
123
+ timeout: tuple = None,
75
124
  allow_redirect: bool = True,
76
125
  proxy_url: str = "",
77
126
  verify: bool = True,
78
127
  stream_callback: StreamCallback = None,
128
+ progress_callback: ProgressCallback = None,
79
129
  verbose: bool = False,
80
130
  ):
81
131
  """
@@ -108,11 +158,9 @@ class Client:
108
158
  headers (``dict[str, str]``, *optional*):
109
159
  Headers to include in the request. Default is ``None``
110
160
 
111
- timeout (``float``, *optional*):
112
- The total timeout for the request in seconds. Default is ``30.0``
113
-
114
- connect_timeout (``float``, *optional*):
115
- The connection timeout for the request in seconds. Default is ``0.0``
161
+ timeout (``tuple``, *optional*):
162
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
163
+ If ``None``, the default timeout specified in ``Client`` is used.
116
164
 
117
165
  allow_redirect (``bool``, *optional*):
118
166
  Whether to allow redirects. Default is ``True``
@@ -126,6 +174,9 @@ class Client:
126
174
  stream_callback (:class:`redc.StreamCallback`, *optional*):
127
175
  Callback for streaming response data. Default is ``None``
128
176
 
177
+ progress_callback (:class:`redc.ProgressCallback`, *optional*):
178
+ Callback for tracking upload and download progress. Default is ``None``
179
+
129
180
  verbose (``bool``, *optional*):
130
181
  Whether to enable verbose output for the request. Default is ``False``
131
182
 
@@ -139,6 +190,12 @@ class Client:
139
190
 
140
191
  stream_callback = stream_callback.callback
141
192
 
193
+ if progress_callback is not None:
194
+ if not isinstance(progress_callback, ProgressCallback):
195
+ raise TypeError("progress_callback must be of type ProgressCallback")
196
+
197
+ progress_callback = progress_callback.callback
198
+
142
199
  if form is not None:
143
200
  if isinstance(form, dict):
144
201
  form = urlencode(form)
@@ -162,6 +219,8 @@ class Client:
162
219
  if not isinstance(files, dict):
163
220
  raise TypeError("files must be of type dict[str, str]")
164
221
 
222
+ timeout, connect_timeout = timeout if timeout is not None else self.__timeout
223
+
165
224
  if timeout <= 0:
166
225
  raise ValueError("timeout must be greater than 0")
167
226
 
@@ -172,9 +231,12 @@ class Client:
172
231
 
173
232
  if headers is not None:
174
233
  if isinstance(headers, dict):
234
+ headers = {**self.__default_headers, **headers}
175
235
  headers = [f"{k}: {v}" for k, v in headers.items()]
176
236
  else:
177
237
  raise TypeError("headers must be of type dict[str, str]")
238
+ else:
239
+ headers = [f"{k}: {v}" for k, v in self.__default_headers.items()]
178
240
 
179
241
  if self.__base_url:
180
242
  url = f"{self.__base_url}{url.lstrip('/')}"
@@ -193,22 +255,25 @@ class Client:
193
255
  allow_redirect=allow_redirect,
194
256
  proxy_url=proxy_url,
195
257
  verify=verify,
258
+ ca_cert_path=self.__ca_cert_path,
196
259
  stream_callback=stream_callback,
260
+ progress_callback=progress_callback,
197
261
  verbose=self.force_verbose or verbose,
198
262
  )
199
- )
263
+ ),
264
+ raise_for_status=self.raise_for_status,
200
265
  )
201
266
 
202
267
  async def get(
203
268
  self,
204
269
  url: str,
205
270
  headers: dict[str, str] = None,
206
- timeout: float = 30.0,
207
- connect_timeout: float = 0.0,
271
+ timeout: tuple = None,
208
272
  allow_redirect: bool = True,
209
273
  proxy_url: str = "",
210
274
  verify: bool = True,
211
275
  stream_callback: StreamCallback = None,
276
+ progress_callback: ProgressCallback = None,
212
277
  verbose: bool = False,
213
278
  ):
214
279
  """
@@ -226,11 +291,9 @@ class Client:
226
291
  headers (``dict[str, str]``, *optional*):
227
292
  Headers to include in the request. Default is ``None``
228
293
 
229
- timeout (``float``, *optional*):
230
- The total timeout for the request in seconds. Default is ``30.0``
231
-
232
- connect_timeout (``float``, *optional*):
233
- The connection timeout for the request in seconds. Default is ``0.0``
294
+ timeout (``tuple``, *optional*):
295
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
296
+ If ``None``, the default timeout specified in ``Client`` is used.
234
297
 
235
298
  allow_redirect (``bool``, *optional*):
236
299
  Whether to allow redirects. Default is ``True``
@@ -244,22 +307,26 @@ class Client:
244
307
  stream_callback (:class:`redc.StreamCallback`, *optional*):
245
308
  Callback for streaming response data. Default is ``None``
246
309
 
310
+ progress_callback (:class:`redc.ProgressCallback`, *optional*):
311
+ Callback for tracking upload and download progress. Default is ``None``
312
+
247
313
  verbose (``bool``, *optional*):
248
314
  Whether to enable verbose output for the request. Default is ``False``
249
315
 
250
316
  Returns:
251
317
  :class:`redc.Response`
252
318
  """
319
+
253
320
  return await self.request(
254
321
  method="GET",
255
322
  url=url,
256
323
  headers=headers,
257
324
  timeout=timeout,
258
- connect_timeout=connect_timeout,
259
325
  allow_redirect=allow_redirect,
260
326
  proxy_url=proxy_url,
261
327
  verify=verify,
262
328
  stream_callback=stream_callback,
329
+ progress_callback=progress_callback,
263
330
  verbose=self.force_verbose or verbose,
264
331
  )
265
332
 
@@ -267,8 +334,7 @@ class Client:
267
334
  self,
268
335
  url: str,
269
336
  headers: dict[str, str] = None,
270
- timeout: float = 30.0,
271
- connect_timeout: float = 0.0,
337
+ timeout: tuple = None,
272
338
  allow_redirect: bool = True,
273
339
  proxy_url: str = "",
274
340
  verify: bool = True,
@@ -289,11 +355,9 @@ class Client:
289
355
  headers (``dict[str, str]``, *optional*):
290
356
  Headers to include in the request. Default is ``None``
291
357
 
292
- timeout (``float``, *optional*):
293
- The total timeout for the request in seconds. Default is ``30.0``
294
-
295
- connect_timeout (``float``, *optional*):
296
- The connection timeout for the request in seconds. Default is ``0.0``
358
+ timeout (``tuple``, *optional*):
359
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
360
+ If ``None``, the default timeout specified in ``Client`` is used.
297
361
 
298
362
  allow_redirect (``bool``, *optional*):
299
363
  Whether to allow redirects. Default is ``True``
@@ -310,12 +374,12 @@ class Client:
310
374
  Returns:
311
375
  :class:`redc.Response`
312
376
  """
377
+
313
378
  return await self.request(
314
379
  method="HEAD",
315
380
  url=url,
316
381
  headers=headers,
317
382
  timeout=timeout,
318
- connect_timeout=connect_timeout,
319
383
  allow_redirect=allow_redirect,
320
384
  proxy_url=proxy_url,
321
385
  verify=verify,
@@ -330,12 +394,12 @@ class Client:
330
394
  data: dict[str, str] = None,
331
395
  files: dict[str, str] = None,
332
396
  headers: dict[str, str] = None,
333
- timeout: float = 30.0,
334
- connect_timeout: float = 0.0,
397
+ timeout: tuple = None,
335
398
  allow_redirect: bool = True,
336
399
  proxy_url: str = "",
337
400
  verify: bool = True,
338
401
  stream_callback: StreamCallback = None,
402
+ progress_callback: ProgressCallback = None,
339
403
  verbose: bool = False,
340
404
  ):
341
405
  """
@@ -369,11 +433,9 @@ class Client:
369
433
  headers (``dict[str, str]``, *optional*):
370
434
  Headers to include in the request. Default is ``None``
371
435
 
372
- timeout (``float``, *optional*):
373
- The total timeout for the request in seconds. Default is ``30.0``
374
-
375
- connect_timeout (``float``, *optional*):
376
- The connection timeout for the request in seconds. Default is ``0.0``
436
+ timeout (``tuple``, *optional*):
437
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
438
+ If ``None``, the default timeout specified in ``Client`` is used.
377
439
 
378
440
  allow_redirect (``bool``, *optional*):
379
441
  Whether to allow redirects. Default is ``True``
@@ -387,12 +449,16 @@ class Client:
387
449
  stream_callback (:class:`redc.StreamCallback`, *optional*):
388
450
  Callback for streaming response data. Default is ``None``
389
451
 
452
+ progress_callback (:class:`redc.ProgressCallback`, *optional*):
453
+ Callback for tracking upload and download progress. Default is ``None``
454
+
390
455
  verbose (``bool``, *optional*):
391
456
  Whether to enable verbose output for the request. Default is ``False``
392
457
 
393
458
  Returns:
394
459
  :class:`redc.Response`
395
460
  """
461
+
396
462
  return await self.request(
397
463
  method="POST",
398
464
  url=url,
@@ -402,11 +468,11 @@ class Client:
402
468
  files=files,
403
469
  headers=headers,
404
470
  timeout=timeout,
405
- connect_timeout=connect_timeout,
406
471
  allow_redirect=allow_redirect,
407
472
  proxy_url=proxy_url,
408
473
  verify=verify,
409
474
  stream_callback=stream_callback,
475
+ progress_callback=progress_callback,
410
476
  verbose=self.force_verbose or verbose,
411
477
  )
412
478
 
@@ -418,12 +484,12 @@ class Client:
418
484
  data: dict[str, str] = None,
419
485
  files: dict[str, str] = None,
420
486
  headers: dict[str, str] = None,
421
- timeout: float = 30.0,
422
- connect_timeout: float = 0.0,
487
+ timeout: tuple = None,
423
488
  allow_redirect: bool = True,
424
489
  proxy_url: str = "",
425
490
  verify: bool = True,
426
491
  stream_callback: StreamCallback = None,
492
+ progress_callback: ProgressCallback = None,
427
493
  verbose: bool = False,
428
494
  ):
429
495
  """
@@ -457,11 +523,9 @@ class Client:
457
523
  headers (``dict[str, str]``, *optional*):
458
524
  Headers to include in the request. Default is ``None``
459
525
 
460
- timeout (``float``, *optional*):
461
- The total timeout for the request in seconds. Default is ``30.0``
462
-
463
- connect_timeout (``float``, *optional*):
464
- The connection timeout for the request in seconds. Default is ``0.0``
526
+ timeout (``tuple``, *optional*):
527
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
528
+ If ``None``, the default timeout specified in ``Client`` is used.
465
529
 
466
530
  allow_redirect (``bool``, *optional*):
467
531
  Whether to allow redirects. Default is ``True``
@@ -475,12 +539,16 @@ class Client:
475
539
  stream_callback (:class:`redc.StreamCallback`, *optional*):
476
540
  Callback for streaming response data. Default is ``None``
477
541
 
542
+ progress_callback (:class:`redc.ProgressCallback`, *optional*):
543
+ Callback for tracking upload and download progress. Default is ``None``
544
+
478
545
  verbose (``bool``, *optional*):
479
546
  Whether to enable verbose output for the request. Default is ``False``
480
547
 
481
548
  Returns:
482
549
  :class:`redc.Response`
483
550
  """
551
+
484
552
  return await self.request(
485
553
  method="PUT",
486
554
  url=url,
@@ -490,11 +558,11 @@ class Client:
490
558
  files=files,
491
559
  headers=headers,
492
560
  timeout=timeout,
493
- connect_timeout=connect_timeout,
494
561
  allow_redirect=allow_redirect,
495
562
  proxy_url=proxy_url,
496
563
  verify=verify,
497
564
  stream_callback=stream_callback,
565
+ progress_callback=progress_callback,
498
566
  verbose=self.force_verbose or verbose,
499
567
  )
500
568
 
@@ -506,12 +574,12 @@ class Client:
506
574
  data: dict[str, str] = None,
507
575
  files: dict[str, str] = None,
508
576
  headers: dict[str, str] = None,
509
- timeout: float = 30.0,
510
- connect_timeout: float = 0.0,
577
+ timeout: tuple = None,
511
578
  allow_redirect: bool = True,
512
579
  proxy_url: str = "",
513
580
  verify: bool = True,
514
581
  stream_callback: StreamCallback = None,
582
+ progress_callback: ProgressCallback = None,
515
583
  verbose: bool = False,
516
584
  ):
517
585
  """
@@ -545,11 +613,9 @@ class Client:
545
613
  headers (``dict[str, str]``, *optional*):
546
614
  Headers to include in the request. Default is ``None``
547
615
 
548
- timeout (``float``, *optional*):
549
- The total timeout for the request in seconds. Default is ``30.0``
550
-
551
- connect_timeout (``float``, *optional*):
552
- The connection timeout for the request in seconds. Default is ``0.0``
616
+ timeout (``tuple``, *optional*):
617
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
618
+ If ``None``, the default timeout specified in ``Client`` is used.
553
619
 
554
620
  allow_redirect (``bool``, *optional*):
555
621
  Whether to allow redirects. Default is ``True``
@@ -563,6 +629,9 @@ class Client:
563
629
  stream_callback (:class:`redc.StreamCallback`, *optional*):
564
630
  Callback for streaming response data. Default is ``None``
565
631
 
632
+ progress_callback (:class:`redc.ProgressCallback`, *optional*):
633
+ Callback for tracking upload and download progress. Default is ``None``
634
+
566
635
  verbose (``bool``, *optional*):
567
636
  Whether to enable verbose output for the request. Default is ``False``
568
637
 
@@ -579,11 +648,11 @@ class Client:
579
648
  files=files,
580
649
  headers=headers,
581
650
  timeout=timeout,
582
- connect_timeout=connect_timeout,
583
651
  allow_redirect=allow_redirect,
584
652
  proxy_url=proxy_url,
585
653
  verify=verify,
586
654
  stream_callback=stream_callback,
655
+ progress_callback=progress_callback,
587
656
  verbose=self.force_verbose or verbose,
588
657
  )
589
658
 
@@ -591,12 +660,12 @@ class Client:
591
660
  self,
592
661
  url: str,
593
662
  headers: dict[str, str] = None,
594
- timeout: float = 30.0,
595
- connect_timeout: float = 0.0,
663
+ timeout: tuple = None,
596
664
  allow_redirect: bool = True,
597
665
  proxy_url: str = "",
598
666
  verify: bool = True,
599
667
  stream_callback: StreamCallback = None,
668
+ progress_callback: ProgressCallback = None,
600
669
  verbose: bool = False,
601
670
  ):
602
671
  """
@@ -614,11 +683,9 @@ class Client:
614
683
  headers (``dict[str, str]``, *optional*):
615
684
  Headers to include in the request. Default is ``None``
616
685
 
617
- timeout (``float``, *optional*):
618
- The total timeout for the request in seconds. Default is ``30.0``
619
-
620
- connect_timeout (``float``, *optional*):
621
- The connection timeout for the request in seconds. Default is ``0.0``
686
+ timeout (``tuple``, *optional*):
687
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
688
+ If ``None``, the default timeout specified in ``Client`` is used.
622
689
 
623
690
  allow_redirect (``bool``, *optional*):
624
691
  Whether to allow redirects. Default is ``True``
@@ -632,22 +699,26 @@ class Client:
632
699
  stream_callback (:class:`redc.StreamCallback`, *optional*):
633
700
  Callback for streaming response data. Default is ``None``
634
701
 
702
+ progress_callback (:class:`redc.ProgressCallback`, *optional*):
703
+ Callback for tracking upload and download progress. Default is ``None``
704
+
635
705
  verbose (``bool``, *optional*):
636
706
  Whether to enable verbose output for the request. Default is ``False``
637
707
 
638
708
  Returns:
639
709
  :class:`redc.Response`
640
710
  """
711
+
641
712
  return await self.request(
642
713
  method="DELETE",
643
714
  url=url,
644
715
  headers=headers,
645
716
  timeout=timeout,
646
- connect_timeout=connect_timeout,
647
717
  allow_redirect=allow_redirect,
648
718
  proxy_url=proxy_url,
649
719
  verify=verify,
650
720
  stream_callback=stream_callback,
721
+ progress_callback=progress_callback,
651
722
  verbose=self.force_verbose or verbose,
652
723
  )
653
724
 
@@ -655,8 +726,7 @@ class Client:
655
726
  self,
656
727
  url: str,
657
728
  headers: dict[str, str] = None,
658
- timeout: float = 30.0,
659
- connect_timeout: float = 0.0,
729
+ timeout: tuple = None,
660
730
  allow_redirect: bool = True,
661
731
  proxy_url: str = "",
662
732
  verify: bool = True,
@@ -677,11 +747,9 @@ class Client:
677
747
  headers (``dict[str, str]``, *optional*):
678
748
  Headers to include in the request. Default is ``None``
679
749
 
680
- timeout (``float``, *optional*):
681
- The total timeout for the request in seconds. Default is ``30.0``
682
-
683
- connect_timeout (``float``, *optional*):
684
- The connection timeout for the request in seconds. Default is ``0.0``
750
+ timeout (``tuple``, *optional*):
751
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
752
+ If ``None``, the default timeout specified in ``Client`` is used.
685
753
 
686
754
  allow_redirect (``bool``, *optional*):
687
755
  Whether to allow redirects. Default is ``True``
@@ -698,12 +766,12 @@ class Client:
698
766
  Returns:
699
767
  :class:`redc.Response`
700
768
  """
769
+
701
770
  return await self.request(
702
771
  method="OPTIONS",
703
772
  url=url,
704
773
  headers=headers,
705
774
  timeout=timeout,
706
- connect_timeout=connect_timeout,
707
775
  allow_redirect=allow_redirect,
708
776
  proxy_url=proxy_url,
709
777
  verify=verify,
@@ -717,4 +785,12 @@ class Client:
717
785
  This method must be called when the client is no longer needed to avoid memory leaks
718
786
  or unexpected behavior
719
787
  """
720
- self.__redc_ext.close()
788
+
789
+ return await self.__loop.run_in_executor(None, self.__redc_ext.close)
790
+
791
+ def __set_default_headers(self):
792
+ if "user-agent" not in self.__default_headers:
793
+ self.__default_headers["user-agent"] = f"redc/{redc.__version__}"
794
+
795
+ if "connection" not in self.__default_headers:
796
+ self.__default_headers["connection"] = "keep-alive"
@@ -0,0 +1,22 @@
1
+ from ..codes import HTTPStatus
2
+
3
+
4
+ class HTTPError(Exception):
5
+ """Exception raised for HTTP and CURL-related errors"""
6
+
7
+ def __init__(self, status_code: int, curl_error_code: int, curl_error_message: str):
8
+ self.status_code = status_code
9
+ self.curl_error_code = curl_error_code
10
+ self.curl_error_message = curl_error_message
11
+ self.is_curl_error = status_code == -1
12
+
13
+ if self.is_curl_error:
14
+ super().__init__(f"CURL {self.curl_error_code}: {self.curl_error_message}")
15
+ else:
16
+ short_description = HTTPStatus.get_description(self.status_code)
17
+
18
+ super().__init__(
19
+ self.status_code
20
+ if not short_description
21
+ else f"{self.status_code} - {short_description}"
22
+ )
@@ -7,6 +7,7 @@ RedC::RedC(const long &buffer) {
7
7
  {
8
8
  acq_gil gil;
9
9
  loop_ = nb::module_::import_("asyncio").attr("get_event_loop")();
10
+ call_soon_threadsafe_ = loop_.attr("call_soon_threadsafe");
10
11
  }
11
12
 
12
13
  static CurlGlobalInit g;
@@ -38,24 +39,26 @@ bool RedC::is_running() {
38
39
  void RedC::close() {
39
40
  if (running_) {
40
41
  running_ = false;
41
- curl_multi_wakeup(multi_handle_);
42
42
 
43
43
  if (worker_thread_.joinable()) {
44
+ curl_multi_wakeup(multi_handle_);
44
45
  worker_thread_.join();
45
46
  }
46
47
 
47
48
  cleanup();
49
+
48
50
  curl_multi_cleanup(multi_handle_);
49
51
  }
50
52
  }
51
53
 
52
- py_object RedC::request(const string &method, const string &url, const char *raw_data, const py_object &data,
54
+ py_object RedC::request(const char *method, const char *url, const char *raw_data, const py_object &data,
53
55
  const py_object &files, const py_object &headers, const long &timeout_ms,
54
- const long &connect_timeout_ms, const bool &allow_redirect, const string &proxy_url,
55
- const bool &verify, const py_object &stream_callback, const bool &verbose) {
56
+ const long &connect_timeout_ms, const bool &allow_redirect, const char *proxy_url,
57
+ const bool &verify, const char *ca_cert_path, const py_object &stream_callback,
58
+ const py_object &progress_callback, const bool &verbose) {
56
59
  CHECK_RUNNING();
57
60
 
58
- if (method.empty() || url.empty()) {
61
+ if (isNullOrEmpty(method) || isNullOrEmpty(url)) {
59
62
  throw std::invalid_argument("method or url must be non-empty");
60
63
  }
61
64
 
@@ -64,12 +67,14 @@ py_object RedC::request(const string &method, const string &url, const char *raw
64
67
  throw std::runtime_error("Failed to create CURL easy handle");
65
68
  }
66
69
 
70
+ bool is_nobody = (strcmp(method, "HEAD") == 0 || strcmp(method, "OPTIONS") == 0);
71
+
67
72
  try {
68
73
  curl_easy_setopt(easy, CURLOPT_BUFFERSIZE, buffer_size_);
69
- curl_easy_setopt(easy, CURLOPT_URL, url.c_str());
70
- curl_easy_setopt(easy, CURLOPT_CUSTOMREQUEST, method.c_str());
74
+ curl_easy_setopt(easy, CURLOPT_URL, url);
75
+ curl_easy_setopt(easy, CURLOPT_CUSTOMREQUEST, method);
76
+ curl_easy_setopt(easy, CURLOPT_NOSIGNAL, 1L);
71
77
 
72
- curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 1L);
73
78
  curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, timeout_ms);
74
79
 
75
80
  curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &RedC::header_callback);
@@ -82,7 +87,7 @@ py_object RedC::request(const string &method, const string &url, const char *raw
82
87
  curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, connect_timeout_ms);
83
88
  }
84
89
 
85
- if (method == "HEAD" || method == "OPTIONS") {
90
+ if (is_nobody) {
86
91
  curl_easy_setopt(easy, CURLOPT_NOBODY, 1L);
87
92
  } else {
88
93
  curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &RedC::write_callback);
@@ -93,17 +98,19 @@ py_object RedC::request(const string &method, const string &url, const char *raw
93
98
  curl_easy_setopt(easy, CURLOPT_MAXREDIRS, 30L);
94
99
  }
95
100
 
96
- if (!proxy_url.empty()) {
97
- curl_easy_setopt(easy, CURLOPT_PROXY, proxy_url.c_str());
101
+ if (!isNullOrEmpty(proxy_url)) {
102
+ curl_easy_setopt(easy, CURLOPT_PROXY, proxy_url);
98
103
  }
99
104
 
100
105
  if (!verify) {
101
106
  curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0);
102
107
  curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 0);
108
+ } else if (!isNullOrEmpty(ca_cert_path)) {
109
+ curl_easy_setopt(easy, CURLOPT_CAINFO, ca_cert_path);
103
110
  }
104
111
 
105
112
  CurlMime curl_mime_;
106
- if (raw_data && *raw_data) {
113
+ if (!isNullOrEmpty(raw_data)) {
107
114
  curl_easy_setopt(easy, CURLOPT_POSTFIELDS, raw_data);
108
115
  curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)strlen(raw_data));
109
116
  } else if (!data.is_none() || !files.is_none()) {
@@ -165,13 +172,25 @@ py_object RedC::request(const string &method, const string &url, const char *raw
165
172
  d.request_headers = std::move(slist_headers);
166
173
  d.curl_mime_ = std::move(curl_mime_);
167
174
 
168
- if (!stream_callback.is_none()) {
169
- d.stream_callback = stream_callback;
170
- }
171
-
172
175
  curl_easy_setopt(easy, CURLOPT_HEADERDATA, &d);
173
- if (method != "HEAD" || method != "OPTIONS") {
176
+ if (!is_nobody) {
174
177
  curl_easy_setopt(easy, CURLOPT_WRITEDATA, &d);
178
+
179
+ if (!stream_callback.is_none()) {
180
+ d.stream_callback = stream_callback;
181
+ d.has_stream_callback = true;
182
+ }
183
+
184
+ if (!progress_callback.is_none()) {
185
+ d.progress_callback = progress_callback;
186
+ d.has_progress_callback = true;
187
+
188
+ curl_easy_setopt(easy, CURLOPT_XFERINFODATA, &d);
189
+ curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 0L);
190
+ curl_easy_setopt(easy, CURLOPT_XFERINFOFUNCTION, &RedC::progress_callback);
191
+ } else {
192
+ curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 1L);
193
+ }
175
194
  }
176
195
  }
177
196
 
@@ -187,9 +206,6 @@ py_object RedC::request(const string &method, const string &url, const char *raw
187
206
 
188
207
  void RedC::worker_loop() {
189
208
  while (running_) {
190
- if (!running_)
191
- break;
192
-
193
209
  CURL *e;
194
210
  if (queue_.try_dequeue(e)) {
195
211
  CURLMcode res = curl_multi_add_handle(multi_handle_, e);
@@ -202,7 +218,7 @@ void RedC::worker_loop() {
202
218
  lock.unlock();
203
219
  {
204
220
  acq_gil gil;
205
- loop_.attr("call_soon_threadsafe")(nb::cpp_function([data = std::move(data), res]() {
221
+ call_soon_threadsafe_(nb::cpp_function([data = std::move(data), res]() {
206
222
  data.future.attr("set_result")(nb::make_tuple(-1, NULL, NULL, (int)res, curl_multi_strerror(res)));
207
223
  }));
208
224
  }
@@ -214,6 +230,10 @@ void RedC::worker_loop() {
214
230
  curl_multi_poll(multi_handle_, nullptr, 0, 30000, &numfds);
215
231
  }
216
232
 
233
+ if (!running_) {
234
+ return;
235
+ }
236
+
217
237
  curl_multi_perform(multi_handle_, &still_running_);
218
238
 
219
239
  CURLMsg *msg;
@@ -257,7 +277,7 @@ void RedC::worker_loop() {
257
277
  result = nb::make_tuple(-1, NULL, NULL, (int)res, curl_easy_strerror(res));
258
278
  }
259
279
 
260
- loop_.attr("call_soon_threadsafe")(nb::cpp_function([data = std::move(data), result = std::move(result)]() {
280
+ call_soon_threadsafe_(nb::cpp_function([data = std::move(data), result = std::move(result)]() {
261
281
  data.future.attr("set_result")(std::move(result));
262
282
  }));
263
283
  }
@@ -272,9 +292,12 @@ void RedC::worker_loop() {
272
292
 
273
293
  void RedC::cleanup() {
274
294
  std::unique_lock<std::mutex> lock(mutex_);
275
- acq_gil gil;
276
295
  for (auto &[easy, data] : transfers_) {
277
- loop_.attr("call_soon_threadsafe")(nb::cpp_function([data = std::move(data)]() { data.future.attr("cancel")(); }));
296
+ {
297
+ acq_gil gil;
298
+ call_soon_threadsafe_(data.future.attr("cancel"));
299
+ }
300
+
278
301
  curl_multi_remove_handle(multi_handle_, easy);
279
302
  curl_easy_cleanup(easy);
280
303
  }
@@ -294,10 +317,25 @@ size_t RedC::header_callback(char *buffer, size_t size, size_t nitems, Data *cli
294
317
  return total_size;
295
318
  }
296
319
 
320
+ size_t RedC::progress_callback(Data *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal,
321
+ curl_off_t ulnow) {
322
+ if (clientp->has_progress_callback) {
323
+ try {
324
+ acq_gil
325
+ gil; //TODO: this sometimes hangs on exit, which lead to other functions to block such as curl_multi_perform and worker_loop never exit
326
+ clientp->progress_callback(dltotal, dlnow, ultotal, ulnow);
327
+ } catch (const std::exception &e) {
328
+ std::cerr << "Error in progress_callback: " << e.what() << std::endl;
329
+ }
330
+ }
331
+
332
+ return 0;
333
+ }
334
+
297
335
  size_t RedC::write_callback(char *data, size_t size, size_t nmemb, Data *clientp) {
298
336
  size_t total_size = size * nmemb;
299
337
 
300
- if (!clientp->stream_callback.is_none()) {
338
+ if (clientp->has_stream_callback) {
301
339
  try {
302
340
  acq_gil gil;
303
341
  clientp->stream_callback(py_bytes(data, total_size), total_size);
@@ -314,9 +352,11 @@ size_t RedC::write_callback(char *data, size_t size, size_t nmemb, Data *clientp
314
352
  NB_MODULE(redc_ext, m) {
315
353
  nb::class_<RedC>(m, "RedC")
316
354
  .def(nb::init<const long &>())
355
+ .def("is_running", &RedC::is_running)
317
356
  .def("request", &RedC::request, arg("method"), arg("url"), arg("raw_data") = "", arg("data") = nb::none(),
318
357
  arg("files") = nb::none(), arg("headers") = nb::none(), arg("timeout_ms") = 60 * 1000,
319
358
  arg("connect_timeout_ms") = 0, arg("allow_redirect") = true, arg("proxy_url") = "", arg("verify") = true,
320
- arg("stream_callback") = nb::none(), arg("verbose") = false)
359
+ arg("ca_cert_path") = "", arg("stream_callback") = nb::none(), arg("progress_callback") = nb::none(),
360
+ arg("verbose") = false)
321
361
  .def("close", &RedC::close);
322
- }
362
+ }
@@ -5,7 +5,6 @@
5
5
  #include <cstring>
6
6
  #include <map>
7
7
  #include <mutex>
8
- #include <string>
9
8
  #include <thread>
10
9
  #include <vector>
11
10
 
@@ -21,17 +20,26 @@
21
20
  namespace nb = nanobind;
22
21
  using namespace nb::literals;
23
22
 
24
- using string = std::string;
25
- using py_object = nb::object;
26
23
  using acq_gil = nb::gil_scoped_acquire;
27
- using arg = nb::arg;
24
+ using rel_gil = nb::gil_scoped_release;
25
+
26
+ using py_object = nb::object;
28
27
  using py_bytes = nb::bytes;
28
+ using arg = nb::arg;
29
29
  using dict = nb::dict;
30
30
 
31
+ bool isNullOrEmpty(const char *str) {
32
+ return !str || !*str;
33
+ }
34
+
31
35
  struct Data {
32
36
  py_object future;
33
37
  py_object loop;
34
- py_object stream_callback = nb::none();
38
+ py_object stream_callback{nb::none()};
39
+ py_object progress_callback{nb::none()};
40
+
41
+ bool has_stream_callback{false};
42
+ bool has_progress_callback{false};
35
43
 
36
44
  std::vector<char> headers;
37
45
  CurlSlist request_headers;
@@ -48,25 +56,25 @@ class RedC {
48
56
  bool is_running();
49
57
  void close();
50
58
 
51
- py_object request(const string &method, const string &url, const char *raw_data = "",
52
- const py_object &data = nb::none(), const py_object &files = nb::none(),
53
- const py_object &headers = nb::none(), const long &timeout_ms = 60 * 1000,
54
- const long &connect_timeout_ms = 0, const bool &allow_redirect = true, const string &proxy_url = "",
55
- const bool &verify = true, const py_object &stream_callback = nb::none(),
56
- const bool &verbose = false);
59
+ py_object request(const char *method, const char *url, const char *raw_data = "", const py_object &data = nb::none(),
60
+ const py_object &files = nb::none(), const py_object &headers = nb::none(),
61
+ const long &timeout_ms = 60 * 1000, const long &connect_timeout_ms = 0,
62
+ const bool &allow_redirect = true, const char *proxy_url = "", const bool &verify = true,
63
+ const char *ca_cert_path = "", const py_object &stream_callback = nb::none(),
64
+ const py_object &progress_callback = nb::none(), const bool &verbose = false);
57
65
 
58
66
  private:
59
- int still_running_ = 0;
67
+ int still_running_{0};
60
68
  long buffer_size_;
61
69
  py_object loop_;
62
- py_object builtins_;
70
+ py_object call_soon_threadsafe_;
63
71
 
64
72
  CURLM *multi_handle_;
65
73
 
66
74
  std::map<CURL *, Data> transfers_;
67
75
  std::mutex mutex_;
68
76
  std::thread worker_thread_;
69
- std::atomic<bool> running_ = false;
77
+ std::atomic<bool> running_{false};
70
78
 
71
79
  moodycamel::ConcurrentQueue<CURL *> queue_;
72
80
 
@@ -75,7 +83,9 @@ class RedC {
75
83
  void CHECK_RUNNING();
76
84
 
77
85
  static size_t header_callback(char *buffer, size_t size, size_t nitems, Data *clientp);
86
+ static size_t progress_callback(Data *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal,
87
+ curl_off_t ulnow);
78
88
  static size_t write_callback(char *data, size_t size, size_t nmemb, Data *clientp);
79
89
  };
80
90
 
81
- #endif // REDC_H
91
+ #endif // REDC_H
@@ -1,6 +1,5 @@
1
1
  from .exceptions import HTTPError
2
2
  from .utils import Headers, json_loads
3
- from .codes import HTTPStatus
4
3
 
5
4
 
6
5
  class Response:
@@ -11,6 +10,7 @@ class Response:
11
10
  response: bytes,
12
11
  curl_code: int,
13
12
  curl_error_message: str,
13
+ raise_for_status: bool = False,
14
14
  ):
15
15
  """Represents an HTTP response of RedC"""
16
16
 
@@ -27,6 +27,9 @@ class Response:
27
27
  self.curl_error_message = curl_error_message
28
28
  """CURL error message"""
29
29
 
30
+ if raise_for_status:
31
+ self.raise_for_status()
32
+
30
33
  @property
31
34
  def content(self) -> bytes:
32
35
  """Returns the raw response content"""
@@ -58,16 +61,8 @@ class Response:
58
61
 
59
62
  def raise_for_status(self):
60
63
  """Raises an HTTPError if the response status indicates an error"""
61
- if self.status_code == -1: # Curl error
62
- raise HTTPError(f"CURL {self.curl_code}: {self.curl_error_message}")
63
-
64
- if 400 <= self.status_code <= 599:
65
- short_description = HTTPStatus.get_description(self.status_code)
66
- raise HTTPError(
67
- self.status_code
68
- if not short_description
69
- else f"{self.status_code} - {short_description}"
70
- )
64
+ if self.status_code == -1 or (400 <= self.status_code <= 599):
65
+ raise HTTPError(self.status_code, self.curl_code, self.curl_error_message)
71
66
 
72
67
  def __bool__(self):
73
68
  return self.status_code != -1 and 200 <= self.status_code <= 299
@@ -1,40 +0,0 @@
1
- import inspect
2
- from typing import Callable
3
-
4
-
5
- class StreamCallback:
6
- """A class for creating a stream callback"""
7
-
8
- def __init__(self, callback: Callable[[bytes, int], None]):
9
- """A callback handler for streaming data
10
-
11
- Example:
12
- .. code-block:: python
13
-
14
- >>> def callback(data: bytes, data_size: int):
15
- ... print(f"Received {len(data)}")
16
- >>> stream_callback = StreamCallback(callback)
17
- >>> client.get("https://example.com/", stream_callback=stream_callback)
18
-
19
- Parameters:
20
- callback (``Callable[[bytes, int], None]``):
21
- A function that accepts two arguments: data (``bytes``) and data_size (``int``)
22
- The function cannot be asynchronous
23
- """
24
-
25
- self.callback = callback
26
- self._validate_callback()
27
-
28
- def _validate_callback(self):
29
- if inspect.iscoroutinefunction(self.callback):
30
- raise TypeError("Callback function cannot be asynchronous")
31
-
32
- signature = inspect.signature(self.callback)
33
-
34
- parameters = signature.parameters
35
- num_parameters = len(parameters)
36
-
37
- if num_parameters != 2:
38
- raise TypeError(
39
- f"Callback function must accept two arguments only callback(data: bytes, data_size: int) but it accepts {num_parameters}."
40
- )
@@ -1,2 +0,0 @@
1
- class HTTPError(Exception):
2
- pass
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes