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