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.
- redc-0.1.1.dev0/.github/workflows/before-all.sh +30 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/PKG-INFO +6 -4
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/README.md +5 -3
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/pyproject.toml +2 -2
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/__init__.py +3 -2
- redc-0.1.1.dev0/redc/callbacks.py +81 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/client.py +120 -69
- redc-0.1.1.dev0/redc/exceptions/__init__.py +22 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/ext/redc.cpp +62 -26
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/ext/redc.h +22 -15
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/response.py +6 -11
- 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
- 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
- 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
- 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
- redc-0.1.0.dev5/redc/callbacks.py +0 -40
- redc-0.1.0.dev5/redc/exceptions/__init__.py +0 -2
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/.clang-format +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/.github/workflows/build_wheels.yml +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/.gitignore +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/CMake/PreventInSourceBuild.cmake +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/CMakeLists.txt +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/LICENSE +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/assets/images/redc-logo.png +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/callback.py +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/codes.py +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/ext/utils/concurrentqueue.h +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/ext/utils/curl_utils.h +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/utils/__init__.py +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/utils/headers.py +0 -0
- {redc-0.1.0.dev5 → redc-0.1.1.dev0}/redc/utils/http.py +0 -0
- {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.
|
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
|
-
[](https://pypi.org/project/RedC) [](https://pepy.tech/project/redc)
|
17
|
+
[](https://pypi.org/project/RedC) [](https://curl.se/ch/8.11.1.html) [](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
|
-
[](https://pypi.org/project/RedC) [](https://pepy.tech/project/redc)
|
5
|
+
[](https://pypi.org/project/RedC) [](https://curl.se/ch/8.11.1.html) [](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.
|
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 = "
|
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.
|
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
|
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
|
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
|
-
|
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:
|
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 (``
|
112
|
-
|
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:
|
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 (``
|
230
|
-
|
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:
|
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 (``
|
293
|
-
|
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:
|
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 (``
|
373
|
-
|
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:
|
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 (``
|
461
|
-
|
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:
|
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 (``
|
549
|
-
|
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:
|
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 (``
|
618
|
-
|
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:
|
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 (``
|
681
|
-
|
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
|
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
|
55
|
-
const bool &verify, const
|
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
|
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
|
70
|
-
curl_easy_setopt(easy, CURLOPT_CUSTOMREQUEST, method
|
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 (
|
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
|
97
|
-
curl_easy_setopt(easy, CURLOPT_PROXY, proxy_url
|
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
|
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 (
|
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
|
-
|
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
|
-
|
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
|
-
|
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("
|
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
|
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
|
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
|
52
|
-
const py_object &
|
53
|
-
const
|
54
|
-
const
|
55
|
-
const
|
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_
|
64
|
+
int still_running_{0};
|
60
65
|
long buffer_size_;
|
61
66
|
py_object loop_;
|
62
|
-
py_object
|
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_
|
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
|
62
|
-
raise HTTPError(
|
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
|
-
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|