redc 0.1.0.dev5__tar.gz → 0.1.1.dev0__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 (32) hide show
  1. redc-0.1.1.dev0/.github/workflows/before-all.sh +30 -0
  2. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/PKG-INFO +6 -4
  3. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/README.md +5 -3
  4. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/pyproject.toml +2 -2
  5. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/__init__.py +3 -2
  6. redc-0.1.1.dev0/redc/callbacks.py +81 -0
  7. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/client.py +120 -69
  8. redc-0.1.1.dev0/redc/exceptions/__init__.py +22 -0
  9. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/ext/redc.cpp +62 -26
  10. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/ext/redc.h +22 -15
  11. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/response.py +6 -11
  12. 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.dev0/wheelhouse/redc-0.1.1.dev0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +0 -0
  13. 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.dev0/wheelhouse/redc-0.1.1.dev0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +0 -0
  14. 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.dev0/wheelhouse/redc-0.1.1.dev0-cp312-abi3-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.dev0/wheelhouse/redc-0.1.1.dev0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +0 -0
  16. redc-0.1.0.dev5/redc/callbacks.py +0 -40
  17. redc-0.1.0.dev5/redc/exceptions/__init__.py +0 -2
  18. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/.clang-format +0 -0
  19. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/.github/workflows/build_wheels.yml +0 -0
  20. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/.gitignore +0 -0
  21. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/CMake/PreventInSourceBuild.cmake +0 -0
  22. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/CMakeLists.txt +0 -0
  23. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/LICENSE +0 -0
  24. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/assets/images/redc-logo.png +0 -0
  25. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/callback.py +0 -0
  26. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/codes.py +0 -0
  27. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/ext/utils/concurrentqueue.h +0 -0
  28. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/ext/utils/curl_utils.h +0 -0
  29. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/utils/__init__.py +0 -0
  30. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/utils/headers.py +0 -0
  31. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/utils/http.py +0 -0
  32. {redc-0.1.0.dev5 → redc-0.1.1.dev0}/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
@@ -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.dev0
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)
@@ -4,7 +4,7 @@ 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.dev0"
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" }]
@@ -27,5 +27,5 @@ skip = "*musllinux*"
27
27
  archs = ["x86_64"]
28
28
 
29
29
  [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"
30
+ before-all = ".github/workflows/before-all.sh"
31
31
  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.dev0"
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,6 +1,6 @@
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
6
  from .utils import json_dumps, parse_base_url
@@ -13,7 +13,11 @@ class Client:
13
13
  self,
14
14
  base_url: str = None,
15
15
  buffer_size: int = 16384,
16
+ headers: dict = None,
17
+ timeout: tuple = (30.0, 0.0),
18
+ ca_cert_path: str = None,
16
19
  force_verbose: bool = None,
20
+ raise_for_status: bool = False,
17
21
  json_encoder=json_dumps,
18
22
  ):
19
23
  """
@@ -32,26 +36,50 @@ class Client:
32
36
  buffer_size (``int``, *optional*):
33
37
  The buffer size for libcurl. Must be greater than ``1024`` bytes. Default is ``16384`` (16KB)
34
38
 
39
+ headers (``dict``, *optional*):
40
+ Headers to include in every request. Default is ``None``
41
+
42
+ timeout (``tuple``, *optional*):
43
+ A tuple of `(total_timeout, connect_timeout)` in seconds to include in every request. Default is ``(30.0, 0.0)``
44
+
45
+ ca_cert_path (``str``, *optional*):
46
+ Path to a CA certificate bundle file for SSL/TLS verification. Default is ``None``
47
+
35
48
  force_verbose (``bool``, *optional*):
36
49
  Force verbose output for all requests. Default is ``None``
37
50
 
51
+ raise_for_status (``bool``, *optional*):
52
+ If ``True``, automatically raises an :class:`redc.HTTPError` for responses with HTTP status codes
53
+ indicating an error (i.e., 4xx or 5xx) or for CURL errors (e.g., network issues, timeouts). Default is ``False``
54
+
38
55
  json_encoder (``Callable`` , *optional*):
39
- A callable for encoding JSON data. Default is ``json_dumps``
56
+ A callable for encoding JSON data. Default is :class:`redc.utils.json_dumps`
40
57
  """
41
58
 
42
59
  assert isinstance(base_url, (str, type(None))), "base_url must be string"
43
60
  assert isinstance(buffer_size, int), "buffer_size must be int"
44
- assert buffer_size >= 1024, "buffer_size must be bigger than 1024 bytes"
45
-
61
+ assert isinstance(ca_cert_path, (str, type(None))), (
62
+ "ca_cert_path must be string"
63
+ )
64
+ assert isinstance(timeout, tuple) and len(timeout) == 2, (
65
+ "timeout must be a tuple of (total_timeout, connect_timeout)"
66
+ )
46
67
  assert isinstance(force_verbose, (bool, type(None))), (
47
68
  "force_verbose must be bool or None"
48
69
  )
70
+ assert isinstance(raise_for_status, bool), "raise_for_status must be bool"
71
+
72
+ assert buffer_size >= 1024, "buffer_size must be bigger than 1024 bytes"
49
73
 
50
74
  self.force_verbose = force_verbose
75
+ self.raise_for_status = raise_for_status
51
76
 
52
77
  self.__base_url = (
53
- None if not isinstance(base_url, str) else parse_base_url(base_url)
78
+ parse_base_url(base_url) if isinstance(base_url, str) else None
54
79
  )
80
+ self.__default_headers = headers if isinstance(headers, dict) else {}
81
+ self.__timeout = timeout
82
+ self.__ca_cert_path = ca_cert_path if isinstance(ca_cert_path, str) else ""
55
83
  self.__json_encoder = json_encoder
56
84
  self.__redc_ext = RedC(buffer_size)
57
85
 
@@ -61,6 +89,16 @@ class Client:
61
89
  async def __aexit__(self, exc_type, exc_val, exc_tb):
62
90
  await self.close()
63
91
 
92
+ @property
93
+ def is_running(self):
94
+ """Checks if RedC is currently running
95
+
96
+ Returns:
97
+ ``bool``: ``True`` if RedC is running, False otherwise
98
+ """
99
+
100
+ return self.__redc_ext.is_running()
101
+
64
102
  async def request(
65
103
  self,
66
104
  method: str,
@@ -70,12 +108,12 @@ class Client:
70
108
  data: dict[str, str] = None,
71
109
  files: dict[str, str] = None,
72
110
  headers: dict[str, str] = None,
73
- timeout: float = 30.0,
74
- connect_timeout: float = 0.0,
111
+ timeout: tuple = None,
75
112
  allow_redirect: bool = True,
76
113
  proxy_url: str = "",
77
114
  verify: bool = True,
78
115
  stream_callback: StreamCallback = None,
116
+ progress_callback: ProgressCallback = None,
79
117
  verbose: bool = False,
80
118
  ):
81
119
  """
@@ -108,11 +146,9 @@ class Client:
108
146
  headers (``dict[str, str]``, *optional*):
109
147
  Headers to include in the request. Default is ``None``
110
148
 
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``
149
+ timeout (``tuple``, *optional*):
150
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
151
+ If ``None``, the default timeout specified in ``Client`` is used.
116
152
 
117
153
  allow_redirect (``bool``, *optional*):
118
154
  Whether to allow redirects. Default is ``True``
@@ -126,6 +162,9 @@ class Client:
126
162
  stream_callback (:class:`redc.StreamCallback`, *optional*):
127
163
  Callback for streaming response data. Default is ``None``
128
164
 
165
+ progress_callback (:class:`redc.ProgressCallback`, *optional*):
166
+ Callback for tracking upload and download progress. Default is ``None``
167
+
129
168
  verbose (``bool``, *optional*):
130
169
  Whether to enable verbose output for the request. Default is ``False``
131
170
 
@@ -139,6 +178,12 @@ class Client:
139
178
 
140
179
  stream_callback = stream_callback.callback
141
180
 
181
+ if progress_callback is not None:
182
+ if not isinstance(progress_callback, ProgressCallback):
183
+ raise TypeError("progress_callback must be of type ProgressCallback")
184
+
185
+ progress_callback = progress_callback.callback
186
+
142
187
  if form is not None:
143
188
  if isinstance(form, dict):
144
189
  form = urlencode(form)
@@ -162,6 +207,8 @@ class Client:
162
207
  if not isinstance(files, dict):
163
208
  raise TypeError("files must be of type dict[str, str]")
164
209
 
210
+ timeout, connect_timeout = timeout if timeout is not None else self.__timeout
211
+
165
212
  if timeout <= 0:
166
213
  raise ValueError("timeout must be greater than 0")
167
214
 
@@ -172,9 +219,12 @@ class Client:
172
219
 
173
220
  if headers is not None:
174
221
  if isinstance(headers, dict):
222
+ headers = {**self.__default_headers, **headers}
175
223
  headers = [f"{k}: {v}" for k, v in headers.items()]
176
224
  else:
177
225
  raise TypeError("headers must be of type dict[str, str]")
226
+ else:
227
+ headers = [f"{k}: {v}" for k, v in self.__default_headers.items()]
178
228
 
179
229
  if self.__base_url:
180
230
  url = f"{self.__base_url}{url.lstrip('/')}"
@@ -193,22 +243,25 @@ class Client:
193
243
  allow_redirect=allow_redirect,
194
244
  proxy_url=proxy_url,
195
245
  verify=verify,
246
+ ca_cert_path=self.__ca_cert_path,
196
247
  stream_callback=stream_callback,
248
+ progress_callback=progress_callback,
197
249
  verbose=self.force_verbose or verbose,
198
250
  )
199
- )
251
+ ),
252
+ raise_for_status=self.raise_for_status,
200
253
  )
201
254
 
202
255
  async def get(
203
256
  self,
204
257
  url: str,
205
258
  headers: dict[str, str] = None,
206
- timeout: float = 30.0,
207
- connect_timeout: float = 0.0,
259
+ timeout: tuple = None,
208
260
  allow_redirect: bool = True,
209
261
  proxy_url: str = "",
210
262
  verify: bool = True,
211
263
  stream_callback: StreamCallback = None,
264
+ progress_callback: ProgressCallback = None,
212
265
  verbose: bool = False,
213
266
  ):
214
267
  """
@@ -226,11 +279,9 @@ class Client:
226
279
  headers (``dict[str, str]``, *optional*):
227
280
  Headers to include in the request. Default is ``None``
228
281
 
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``
282
+ timeout (``tuple``, *optional*):
283
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
284
+ If ``None``, the default timeout specified in ``Client`` is used.
234
285
 
235
286
  allow_redirect (``bool``, *optional*):
236
287
  Whether to allow redirects. Default is ``True``
@@ -244,22 +295,26 @@ class Client:
244
295
  stream_callback (:class:`redc.StreamCallback`, *optional*):
245
296
  Callback for streaming response data. Default is ``None``
246
297
 
298
+ progress_callback (:class:`redc.ProgressCallback`, *optional*):
299
+ Callback for tracking upload and download progress. Default is ``None``
300
+
247
301
  verbose (``bool``, *optional*):
248
302
  Whether to enable verbose output for the request. Default is ``False``
249
303
 
250
304
  Returns:
251
305
  :class:`redc.Response`
252
306
  """
307
+
253
308
  return await self.request(
254
309
  method="GET",
255
310
  url=url,
256
311
  headers=headers,
257
312
  timeout=timeout,
258
- connect_timeout=connect_timeout,
259
313
  allow_redirect=allow_redirect,
260
314
  proxy_url=proxy_url,
261
315
  verify=verify,
262
316
  stream_callback=stream_callback,
317
+ progress_callback=progress_callback,
263
318
  verbose=self.force_verbose or verbose,
264
319
  )
265
320
 
@@ -267,8 +322,7 @@ class Client:
267
322
  self,
268
323
  url: str,
269
324
  headers: dict[str, str] = None,
270
- timeout: float = 30.0,
271
- connect_timeout: float = 0.0,
325
+ timeout: tuple = None,
272
326
  allow_redirect: bool = True,
273
327
  proxy_url: str = "",
274
328
  verify: bool = True,
@@ -289,11 +343,9 @@ class Client:
289
343
  headers (``dict[str, str]``, *optional*):
290
344
  Headers to include in the request. Default is ``None``
291
345
 
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``
346
+ timeout (``tuple``, *optional*):
347
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
348
+ If ``None``, the default timeout specified in ``Client`` is used.
297
349
 
298
350
  allow_redirect (``bool``, *optional*):
299
351
  Whether to allow redirects. Default is ``True``
@@ -315,7 +367,6 @@ class Client:
315
367
  url=url,
316
368
  headers=headers,
317
369
  timeout=timeout,
318
- connect_timeout=connect_timeout,
319
370
  allow_redirect=allow_redirect,
320
371
  proxy_url=proxy_url,
321
372
  verify=verify,
@@ -330,12 +381,12 @@ class Client:
330
381
  data: dict[str, str] = None,
331
382
  files: dict[str, str] = None,
332
383
  headers: dict[str, str] = None,
333
- timeout: float = 30.0,
334
- connect_timeout: float = 0.0,
384
+ timeout: tuple = None,
335
385
  allow_redirect: bool = True,
336
386
  proxy_url: str = "",
337
387
  verify: bool = True,
338
388
  stream_callback: StreamCallback = None,
389
+ progress_callback: ProgressCallback = None,
339
390
  verbose: bool = False,
340
391
  ):
341
392
  """
@@ -369,11 +420,9 @@ class Client:
369
420
  headers (``dict[str, str]``, *optional*):
370
421
  Headers to include in the request. Default is ``None``
371
422
 
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``
423
+ timeout (``tuple``, *optional*):
424
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
425
+ If ``None``, the default timeout specified in ``Client`` is used.
377
426
 
378
427
  allow_redirect (``bool``, *optional*):
379
428
  Whether to allow redirects. Default is ``True``
@@ -387,6 +436,9 @@ class Client:
387
436
  stream_callback (:class:`redc.StreamCallback`, *optional*):
388
437
  Callback for streaming response data. Default is ``None``
389
438
 
439
+ progress_callback (:class:`redc.ProgressCallback`, *optional*):
440
+ Callback for tracking upload and download progress. Default is ``None``
441
+
390
442
  verbose (``bool``, *optional*):
391
443
  Whether to enable verbose output for the request. Default is ``False``
392
444
 
@@ -402,11 +454,11 @@ class Client:
402
454
  files=files,
403
455
  headers=headers,
404
456
  timeout=timeout,
405
- connect_timeout=connect_timeout,
406
457
  allow_redirect=allow_redirect,
407
458
  proxy_url=proxy_url,
408
459
  verify=verify,
409
460
  stream_callback=stream_callback,
461
+ progress_callback=progress_callback,
410
462
  verbose=self.force_verbose or verbose,
411
463
  )
412
464
 
@@ -418,12 +470,12 @@ class Client:
418
470
  data: dict[str, str] = None,
419
471
  files: dict[str, str] = None,
420
472
  headers: dict[str, str] = None,
421
- timeout: float = 30.0,
422
- connect_timeout: float = 0.0,
473
+ timeout: tuple = None,
423
474
  allow_redirect: bool = True,
424
475
  proxy_url: str = "",
425
476
  verify: bool = True,
426
477
  stream_callback: StreamCallback = None,
478
+ progress_callback: ProgressCallback = None,
427
479
  verbose: bool = False,
428
480
  ):
429
481
  """
@@ -457,11 +509,9 @@ class Client:
457
509
  headers (``dict[str, str]``, *optional*):
458
510
  Headers to include in the request. Default is ``None``
459
511
 
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``
512
+ timeout (``tuple``, *optional*):
513
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
514
+ If ``None``, the default timeout specified in ``Client`` is used.
465
515
 
466
516
  allow_redirect (``bool``, *optional*):
467
517
  Whether to allow redirects. Default is ``True``
@@ -475,6 +525,9 @@ class Client:
475
525
  stream_callback (:class:`redc.StreamCallback`, *optional*):
476
526
  Callback for streaming response data. Default is ``None``
477
527
 
528
+ progress_callback (:class:`redc.ProgressCallback`, *optional*):
529
+ Callback for tracking upload and download progress. Default is ``None``
530
+
478
531
  verbose (``bool``, *optional*):
479
532
  Whether to enable verbose output for the request. Default is ``False``
480
533
 
@@ -490,11 +543,11 @@ class Client:
490
543
  files=files,
491
544
  headers=headers,
492
545
  timeout=timeout,
493
- connect_timeout=connect_timeout,
494
546
  allow_redirect=allow_redirect,
495
547
  proxy_url=proxy_url,
496
548
  verify=verify,
497
549
  stream_callback=stream_callback,
550
+ progress_callback=progress_callback,
498
551
  verbose=self.force_verbose or verbose,
499
552
  )
500
553
 
@@ -506,12 +559,12 @@ class Client:
506
559
  data: dict[str, str] = None,
507
560
  files: dict[str, str] = None,
508
561
  headers: dict[str, str] = None,
509
- timeout: float = 30.0,
510
- connect_timeout: float = 0.0,
562
+ timeout: tuple = None,
511
563
  allow_redirect: bool = True,
512
564
  proxy_url: str = "",
513
565
  verify: bool = True,
514
566
  stream_callback: StreamCallback = None,
567
+ progress_callback: ProgressCallback = None,
515
568
  verbose: bool = False,
516
569
  ):
517
570
  """
@@ -545,11 +598,9 @@ class Client:
545
598
  headers (``dict[str, str]``, *optional*):
546
599
  Headers to include in the request. Default is ``None``
547
600
 
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``
601
+ timeout (``tuple``, *optional*):
602
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
603
+ If ``None``, the default timeout specified in ``Client`` is used.
553
604
 
554
605
  allow_redirect (``bool``, *optional*):
555
606
  Whether to allow redirects. Default is ``True``
@@ -563,6 +614,9 @@ class Client:
563
614
  stream_callback (:class:`redc.StreamCallback`, *optional*):
564
615
  Callback for streaming response data. Default is ``None``
565
616
 
617
+ progress_callback (:class:`redc.ProgressCallback`, *optional*):
618
+ Callback for tracking upload and download progress. Default is ``None``
619
+
566
620
  verbose (``bool``, *optional*):
567
621
  Whether to enable verbose output for the request. Default is ``False``
568
622
 
@@ -579,11 +633,11 @@ class Client:
579
633
  files=files,
580
634
  headers=headers,
581
635
  timeout=timeout,
582
- connect_timeout=connect_timeout,
583
636
  allow_redirect=allow_redirect,
584
637
  proxy_url=proxy_url,
585
638
  verify=verify,
586
639
  stream_callback=stream_callback,
640
+ progress_callback=progress_callback,
587
641
  verbose=self.force_verbose or verbose,
588
642
  )
589
643
 
@@ -591,12 +645,12 @@ class Client:
591
645
  self,
592
646
  url: str,
593
647
  headers: dict[str, str] = None,
594
- timeout: float = 30.0,
595
- connect_timeout: float = 0.0,
648
+ timeout: tuple = None,
596
649
  allow_redirect: bool = True,
597
650
  proxy_url: str = "",
598
651
  verify: bool = True,
599
652
  stream_callback: StreamCallback = None,
653
+ progress_callback: ProgressCallback = None,
600
654
  verbose: bool = False,
601
655
  ):
602
656
  """
@@ -614,11 +668,9 @@ class Client:
614
668
  headers (``dict[str, str]``, *optional*):
615
669
  Headers to include in the request. Default is ``None``
616
670
 
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``
671
+ timeout (``tuple``, *optional*):
672
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
673
+ If ``None``, the default timeout specified in ``Client`` is used.
622
674
 
623
675
  allow_redirect (``bool``, *optional*):
624
676
  Whether to allow redirects. Default is ``True``
@@ -632,6 +684,9 @@ class Client:
632
684
  stream_callback (:class:`redc.StreamCallback`, *optional*):
633
685
  Callback for streaming response data. Default is ``None``
634
686
 
687
+ progress_callback (:class:`redc.ProgressCallback`, *optional*):
688
+ Callback for tracking upload and download progress. Default is ``None``
689
+
635
690
  verbose (``bool``, *optional*):
636
691
  Whether to enable verbose output for the request. Default is ``False``
637
692
 
@@ -643,11 +698,11 @@ class Client:
643
698
  url=url,
644
699
  headers=headers,
645
700
  timeout=timeout,
646
- connect_timeout=connect_timeout,
647
701
  allow_redirect=allow_redirect,
648
702
  proxy_url=proxy_url,
649
703
  verify=verify,
650
704
  stream_callback=stream_callback,
705
+ progress_callback=progress_callback,
651
706
  verbose=self.force_verbose or verbose,
652
707
  )
653
708
 
@@ -655,8 +710,7 @@ class Client:
655
710
  self,
656
711
  url: str,
657
712
  headers: dict[str, str] = None,
658
- timeout: float = 30.0,
659
- connect_timeout: float = 0.0,
713
+ timeout: tuple = None,
660
714
  allow_redirect: bool = True,
661
715
  proxy_url: str = "",
662
716
  verify: bool = True,
@@ -677,11 +731,9 @@ class Client:
677
731
  headers (``dict[str, str]``, *optional*):
678
732
  Headers to include in the request. Default is ``None``
679
733
 
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``
734
+ timeout (``tuple``, *optional*):
735
+ A tuple of ``(total_timeout, connect_timeout)`` in seconds to override the default timeout.
736
+ If ``None``, the default timeout specified in ``Client`` is used.
685
737
 
686
738
  allow_redirect (``bool``, *optional*):
687
739
  Whether to allow redirects. Default is ``True``
@@ -703,7 +755,6 @@ class Client:
703
755
  url=url,
704
756
  headers=headers,
705
757
  timeout=timeout,
706
- connect_timeout=connect_timeout,
707
758
  allow_redirect=allow_redirect,
708
759
  proxy_url=proxy_url,
709
760
  verify=verify,
@@ -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,13 @@ 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);
71
76
 
72
- curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 1L);
73
77
  curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, timeout_ms);
74
78
 
75
79
  curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &RedC::header_callback);
@@ -82,7 +86,7 @@ py_object RedC::request(const string &method, const string &url, const char *raw
82
86
  curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, connect_timeout_ms);
83
87
  }
84
88
 
85
- if (method == "HEAD" || method == "OPTIONS") {
89
+ if (is_nobody) {
86
90
  curl_easy_setopt(easy, CURLOPT_NOBODY, 1L);
87
91
  } else {
88
92
  curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &RedC::write_callback);
@@ -93,17 +97,19 @@ py_object RedC::request(const string &method, const string &url, const char *raw
93
97
  curl_easy_setopt(easy, CURLOPT_MAXREDIRS, 30L);
94
98
  }
95
99
 
96
- if (!proxy_url.empty()) {
97
- curl_easy_setopt(easy, CURLOPT_PROXY, proxy_url.c_str());
100
+ if (!isNullOrEmpty(proxy_url)) {
101
+ curl_easy_setopt(easy, CURLOPT_PROXY, proxy_url);
98
102
  }
99
103
 
100
104
  if (!verify) {
101
105
  curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0);
102
106
  curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 0);
107
+ } else if (!isNullOrEmpty(ca_cert_path)) {
108
+ curl_easy_setopt(easy, CURLOPT_CAINFO, ca_cert_path);
103
109
  }
104
110
 
105
111
  CurlMime curl_mime_;
106
- if (raw_data && *raw_data) {
112
+ if (!isNullOrEmpty(raw_data)) {
107
113
  curl_easy_setopt(easy, CURLOPT_POSTFIELDS, raw_data);
108
114
  curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)strlen(raw_data));
109
115
  } else if (!data.is_none() || !files.is_none()) {
@@ -165,13 +171,23 @@ py_object RedC::request(const string &method, const string &url, const char *raw
165
171
  d.request_headers = std::move(slist_headers);
166
172
  d.curl_mime_ = std::move(curl_mime_);
167
173
 
168
- if (!stream_callback.is_none()) {
169
- d.stream_callback = stream_callback;
170
- }
171
-
172
174
  curl_easy_setopt(easy, CURLOPT_HEADERDATA, &d);
173
- if (method != "HEAD" || method != "OPTIONS") {
175
+ if (!is_nobody) {
174
176
  curl_easy_setopt(easy, CURLOPT_WRITEDATA, &d);
177
+
178
+ if (!stream_callback.is_none()) {
179
+ d.stream_callback = stream_callback;
180
+ }
181
+
182
+ if (!progress_callback.is_none()) {
183
+ d.progress_callback = progress_callback;
184
+
185
+ curl_easy_setopt(easy, CURLOPT_XFERINFODATA, &d);
186
+ curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 0L);
187
+ curl_easy_setopt(easy, CURLOPT_XFERINFOFUNCTION, &RedC::progress_callback);
188
+ } else {
189
+ curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 1L);
190
+ }
175
191
  }
176
192
  }
177
193
 
@@ -187,9 +203,6 @@ py_object RedC::request(const string &method, const string &url, const char *raw
187
203
 
188
204
  void RedC::worker_loop() {
189
205
  while (running_) {
190
- if (!running_)
191
- break;
192
-
193
206
  CURL *e;
194
207
  if (queue_.try_dequeue(e)) {
195
208
  CURLMcode res = curl_multi_add_handle(multi_handle_, e);
@@ -202,7 +215,7 @@ void RedC::worker_loop() {
202
215
  lock.unlock();
203
216
  {
204
217
  acq_gil gil;
205
- loop_.attr("call_soon_threadsafe")(nb::cpp_function([data = std::move(data), res]() {
218
+ call_soon_threadsafe_(nb::cpp_function([data = std::move(data), res]() {
206
219
  data.future.attr("set_result")(nb::make_tuple(-1, NULL, NULL, (int)res, curl_multi_strerror(res)));
207
220
  }));
208
221
  }
@@ -214,6 +227,10 @@ void RedC::worker_loop() {
214
227
  curl_multi_poll(multi_handle_, nullptr, 0, 30000, &numfds);
215
228
  }
216
229
 
230
+ if (!running_) {
231
+ return;
232
+ }
233
+
217
234
  curl_multi_perform(multi_handle_, &still_running_);
218
235
 
219
236
  CURLMsg *msg;
@@ -257,7 +274,7 @@ void RedC::worker_loop() {
257
274
  result = nb::make_tuple(-1, NULL, NULL, (int)res, curl_easy_strerror(res));
258
275
  }
259
276
 
260
- loop_.attr("call_soon_threadsafe")(nb::cpp_function([data = std::move(data), result = std::move(result)]() {
277
+ call_soon_threadsafe_(nb::cpp_function([data = std::move(data), result = std::move(result)]() {
261
278
  data.future.attr("set_result")(std::move(result));
262
279
  }));
263
280
  }
@@ -272,9 +289,12 @@ void RedC::worker_loop() {
272
289
 
273
290
  void RedC::cleanup() {
274
291
  std::unique_lock<std::mutex> lock(mutex_);
275
- acq_gil gil;
276
292
  for (auto &[easy, data] : transfers_) {
277
- loop_.attr("call_soon_threadsafe")(nb::cpp_function([data = std::move(data)]() { data.future.attr("cancel")(); }));
293
+ {
294
+ acq_gil gil;
295
+ call_soon_threadsafe_(data.future.attr("cancel"));
296
+ }
297
+
278
298
  curl_multi_remove_handle(multi_handle_, easy);
279
299
  curl_easy_cleanup(easy);
280
300
  }
@@ -294,6 +314,20 @@ size_t RedC::header_callback(char *buffer, size_t size, size_t nitems, Data *cli
294
314
  return total_size;
295
315
  }
296
316
 
317
+ size_t RedC::progress_callback(Data *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal,
318
+ curl_off_t ulnow) {
319
+ if (!clientp->progress_callback.is_none()) {
320
+ try {
321
+ acq_gil gil;
322
+ clientp->progress_callback(dltotal, dlnow, ultotal, ulnow);
323
+ } catch (const std::exception &e) {
324
+ std::cerr << "Error in progress_callback: " << e.what() << std::endl;
325
+ }
326
+ }
327
+
328
+ return 0;
329
+ }
330
+
297
331
  size_t RedC::write_callback(char *data, size_t size, size_t nmemb, Data *clientp) {
298
332
  size_t total_size = size * nmemb;
299
333
 
@@ -314,9 +348,11 @@ size_t RedC::write_callback(char *data, size_t size, size_t nmemb, Data *clientp
314
348
  NB_MODULE(redc_ext, m) {
315
349
  nb::class_<RedC>(m, "RedC")
316
350
  .def(nb::init<const long &>())
351
+ .def("is_running", &RedC::is_running)
317
352
  .def("request", &RedC::request, arg("method"), arg("url"), arg("raw_data") = "", arg("data") = nb::none(),
318
353
  arg("files") = nb::none(), arg("headers") = nb::none(), arg("timeout_ms") = 60 * 1000,
319
354
  arg("connect_timeout_ms") = 0, arg("allow_redirect") = true, arg("proxy_url") = "", arg("verify") = true,
320
- arg("stream_callback") = nb::none(), arg("verbose") = false)
355
+ arg("ca_cert_path") = "", arg("stream_callback") = nb::none(), arg("progress_callback") = nb::none(),
356
+ arg("verbose") = false)
321
357
  .def("close", &RedC::close);
322
- }
358
+ }
@@ -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,23 @@
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()};
35
40
 
36
41
  std::vector<char> headers;
37
42
  CurlSlist request_headers;
@@ -48,25 +53,25 @@ class RedC {
48
53
  bool is_running();
49
54
  void close();
50
55
 
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);
56
+ py_object request(const char *method, const char *url, const char *raw_data = "", const py_object &data = nb::none(),
57
+ const py_object &files = nb::none(), const py_object &headers = nb::none(),
58
+ const long &timeout_ms = 60 * 1000, const long &connect_timeout_ms = 0,
59
+ const bool &allow_redirect = true, const char *proxy_url = "", const bool &verify = true,
60
+ const char *ca_cert_path = "", const py_object &stream_callback = nb::none(),
61
+ const py_object &progress_callback = nb::none(), const bool &verbose = false);
57
62
 
58
63
  private:
59
- int still_running_ = 0;
64
+ int still_running_{0};
60
65
  long buffer_size_;
61
66
  py_object loop_;
62
- py_object builtins_;
67
+ py_object call_soon_threadsafe_;
63
68
 
64
69
  CURLM *multi_handle_;
65
70
 
66
71
  std::map<CURL *, Data> transfers_;
67
72
  std::mutex mutex_;
68
73
  std::thread worker_thread_;
69
- std::atomic<bool> running_ = false;
74
+ std::atomic<bool> running_{false};
70
75
 
71
76
  moodycamel::ConcurrentQueue<CURL *> queue_;
72
77
 
@@ -75,7 +80,9 @@ class RedC {
75
80
  void CHECK_RUNNING();
76
81
 
77
82
  static size_t header_callback(char *buffer, size_t size, size_t nitems, Data *clientp);
83
+ static size_t progress_callback(Data *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal,
84
+ curl_off_t ulnow);
78
85
  static size_t write_callback(char *data, size_t size, size_t nmemb, Data *clientp);
79
86
  };
80
87
 
81
- #endif // REDC_H
88
+ #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
File without changes