redc 0.1.1.dev1__cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
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/__init__.py +22 -0
- redc/callback.py +19 -0
- redc/callbacks.py +81 -0
- redc/client.py +796 -0
- redc/codes.py +151 -0
- redc/exceptions/__init__.py +22 -0
- redc/ext/redc.cpp +362 -0
- redc/ext/redc.h +91 -0
- redc/ext/utils/concurrentqueue.h +3747 -0
- redc/ext/utils/curl_utils.h +84 -0
- redc/redc_ext.cpython-313t-x86_64-linux-gnu.so +0 -0
- redc/response.py +68 -0
- redc/utils/__init__.py +5 -0
- redc/utils/headers.py +60 -0
- redc/utils/http.py +12 -0
- redc/utils/json_encoder.py +17 -0
- redc-0.1.1.dev1.dist-info/METADATA +64 -0
- redc-0.1.1.dev1.dist-info/RECORD +27 -0
- redc-0.1.1.dev1.dist-info/WHEEL +6 -0
- redc-0.1.1.dev1.dist-info/licenses/LICENSE +21 -0
- redc.libs/libcrypto-cff6b41d.so.3 +0 -0
- redc.libs/libcurl-bf2af365.so.4.8.0 +0 -0
- redc.libs/libicudata-cb3ba60c.so.50.2 +0 -0
- redc.libs/libicuuc-1796a535.so.50.2 +0 -0
- redc.libs/libnghttp2-cc579893.so.14.17.0 +0 -0
- redc.libs/libpsl-9527e555.so.0.2.4 +0 -0
- redc.libs/libssl-ebec6e5c.so.3 +0 -0
redc/codes.py
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
status_descriptions = {
|
2
|
+
# Informational (1xx)
|
3
|
+
100: "Continue",
|
4
|
+
101: "Switching Protocols",
|
5
|
+
102: "Processing",
|
6
|
+
103: "Early Hints",
|
7
|
+
# Success (2xx)
|
8
|
+
200: "OK",
|
9
|
+
201: "Created",
|
10
|
+
202: "Accepted",
|
11
|
+
203: "Non-Authoritative Information",
|
12
|
+
204: "No Content",
|
13
|
+
205: "Reset Content",
|
14
|
+
206: "Partial Content",
|
15
|
+
207: "Multi-Status",
|
16
|
+
208: "Already Reported",
|
17
|
+
226: "IM Used",
|
18
|
+
# Redirection (3xx)
|
19
|
+
300: "Multiple Choices",
|
20
|
+
301: "Moved Permanently",
|
21
|
+
302: "Found",
|
22
|
+
303: "See Other",
|
23
|
+
304: "Not Modified",
|
24
|
+
305: "Use Proxy",
|
25
|
+
307: "Temporary Redirect",
|
26
|
+
308: "Permanent Redirect",
|
27
|
+
# Client Errors (4xx)
|
28
|
+
400: "Bad Request",
|
29
|
+
401: "Unauthorized",
|
30
|
+
402: "Payment Required",
|
31
|
+
403: "Forbidden",
|
32
|
+
404: "Not Found",
|
33
|
+
405: "Method Not Allowed",
|
34
|
+
406: "Not Acceptable",
|
35
|
+
407: "Proxy Authentication Required",
|
36
|
+
408: "Request Timeout",
|
37
|
+
409: "Conflict",
|
38
|
+
410: "Gone",
|
39
|
+
411: "Length Required",
|
40
|
+
412: "Precondition Failed",
|
41
|
+
413: "Payload Too Large",
|
42
|
+
414: "URI Too Long",
|
43
|
+
415: "Unsupported Media Type",
|
44
|
+
416: "Range Not Satisfiable",
|
45
|
+
417: "Expectation Failed",
|
46
|
+
418: "I'm a Teapot",
|
47
|
+
421: "Misdirected Request",
|
48
|
+
422: "Unprocessable Entity",
|
49
|
+
423: "Locked",
|
50
|
+
424: "Failed Dependency",
|
51
|
+
425: "Too Early",
|
52
|
+
426: "Upgrade Required",
|
53
|
+
428: "Precondition Required",
|
54
|
+
429: "Too Many Requests",
|
55
|
+
431: "Request Header Fields Too Large",
|
56
|
+
451: "Unavailable For Legal Reasons",
|
57
|
+
# Server Errors (5xx)
|
58
|
+
500: "Internal Server Error",
|
59
|
+
501: "Not Implemented",
|
60
|
+
502: "Bad Gateway",
|
61
|
+
503: "Service Unavailable",
|
62
|
+
504: "Gateway Timeout",
|
63
|
+
505: "HTTP Version Not Supported",
|
64
|
+
506: "Variant Also Negotiates",
|
65
|
+
507: "Insufficient Storage",
|
66
|
+
508: "Loop Detected",
|
67
|
+
510: "Not Extended",
|
68
|
+
511: "Network Authentication Required",
|
69
|
+
}
|
70
|
+
|
71
|
+
|
72
|
+
class HTTPStatus:
|
73
|
+
# Informational (1xx)
|
74
|
+
CONTINUE = 100
|
75
|
+
SWITCHING_PROTOCOLS = 101
|
76
|
+
PROCESSING = 102
|
77
|
+
EARLY_HINTS = 103
|
78
|
+
|
79
|
+
# Success (2xx)
|
80
|
+
OK = 200
|
81
|
+
CREATED = 201
|
82
|
+
ACCEPTED = 202
|
83
|
+
NON_AUTHORITATIVE_INFORMATION = 203
|
84
|
+
NO_CONTENT = 204
|
85
|
+
RESET_CONTENT = 205
|
86
|
+
PARTIAL_CONTENT = 206
|
87
|
+
MULTI_STATUS = 207
|
88
|
+
ALREADY_REPORTED = 208
|
89
|
+
IM_USED = 226
|
90
|
+
|
91
|
+
# Redirection (3xx)
|
92
|
+
MULTIPLE_CHOICES = 300
|
93
|
+
MOVED_PERMANENTLY = 301
|
94
|
+
FOUND = 302
|
95
|
+
SEE_OTHER = 303
|
96
|
+
NOT_MODIFIED = 304
|
97
|
+
USE_PROXY = 305
|
98
|
+
TEMPORARY_REDIRECT = 307
|
99
|
+
PERMANENT_REDIRECT = 308
|
100
|
+
|
101
|
+
# Client Errors (4xx)
|
102
|
+
BAD_REQUEST = 400
|
103
|
+
UNAUTHORIZED = 401
|
104
|
+
PAYMENT_REQUIRED = 402
|
105
|
+
FORBIDDEN = 403
|
106
|
+
NOT_FOUND = 404
|
107
|
+
METHOD_NOT_ALLOWED = 405
|
108
|
+
NOT_ACCEPTABLE = 406
|
109
|
+
PROXY_AUTHENTICATION_REQUIRED = 407
|
110
|
+
REQUEST_TIMEOUT = 408
|
111
|
+
CONFLICT = 409
|
112
|
+
GONE = 410
|
113
|
+
LENGTH_REQUIRED = 411
|
114
|
+
PRECONDITION_FAILED = 412
|
115
|
+
PAYLOAD_TOO_LARGE = 413
|
116
|
+
URI_TOO_LONG = 414
|
117
|
+
UNSUPPORTED_MEDIA_TYPE = 415
|
118
|
+
RANGE_NOT_SATISFIABLE = 416
|
119
|
+
EXPECTATION_FAILED = 417
|
120
|
+
IM_A_TEAPOT = 418
|
121
|
+
MISDIRECTED_REQUEST = 421
|
122
|
+
UNPROCESSABLE_ENTITY = 422
|
123
|
+
LOCKED = 423
|
124
|
+
FAILED_DEPENDENCY = 424
|
125
|
+
TOO_EARLY = 425
|
126
|
+
UPGRADE_REQUIRED = 426
|
127
|
+
PRECONDITION_REQUIRED = 428
|
128
|
+
TOO_MANY_REQUESTS = 429
|
129
|
+
REQUEST_HEADER_FIELDS_TOO_LARGE = 431
|
130
|
+
UNAVAILABLE_FOR_LEGAL_REASONS = 451
|
131
|
+
|
132
|
+
# Server Errors (5xx)
|
133
|
+
INTERNAL_SERVER_ERROR = 500
|
134
|
+
NOT_IMPLEMENTED = 501
|
135
|
+
BAD_GATEWAY = 502
|
136
|
+
SERVICE_UNAVAILABLE = 503
|
137
|
+
GATEWAY_TIMEOUT = 504
|
138
|
+
HTTP_VERSION_NOT_SUPPORTED = 505
|
139
|
+
VARIANT_ALSO_NEGOTIATES = 506
|
140
|
+
INSUFFICIENT_STORAGE = 507
|
141
|
+
LOOP_DETECTED = 508
|
142
|
+
NOT_EXTENDED = 510
|
143
|
+
NETWORK_AUTHENTICATION_REQUIRED = 511
|
144
|
+
|
145
|
+
@staticmethod
|
146
|
+
def get_description(status_code: int) -> str:
|
147
|
+
"""
|
148
|
+
Get the description of a status code
|
149
|
+
"""
|
150
|
+
|
151
|
+
return status_descriptions.get(status_code, "")
|
@@ -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
|
+
)
|
redc/ext/redc.cpp
ADDED
@@ -0,0 +1,362 @@
|
|
1
|
+
#include "redc.h"
|
2
|
+
#include "utils/curl_utils.h"
|
3
|
+
#include <iostream>
|
4
|
+
#include <stdexcept>
|
5
|
+
|
6
|
+
RedC::RedC(const long &buffer) {
|
7
|
+
{
|
8
|
+
acq_gil gil;
|
9
|
+
loop_ = nb::module_::import_("asyncio").attr("get_event_loop")();
|
10
|
+
call_soon_threadsafe_ = loop_.attr("call_soon_threadsafe");
|
11
|
+
}
|
12
|
+
|
13
|
+
static CurlGlobalInit g;
|
14
|
+
|
15
|
+
buffer_size_ = buffer;
|
16
|
+
multi_handle_ = curl_multi_init();
|
17
|
+
|
18
|
+
if (!multi_handle_) {
|
19
|
+
throw std::runtime_error("Failed to create CURL multi handle");
|
20
|
+
}
|
21
|
+
|
22
|
+
try {
|
23
|
+
running_ = true;
|
24
|
+
worker_thread_ = std::thread(&RedC::worker_loop, this);
|
25
|
+
} catch (...) {
|
26
|
+
curl_multi_cleanup(multi_handle_);
|
27
|
+
throw;
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
RedC::~RedC() {
|
32
|
+
this->close();
|
33
|
+
}
|
34
|
+
|
35
|
+
bool RedC::is_running() {
|
36
|
+
return running_;
|
37
|
+
}
|
38
|
+
|
39
|
+
void RedC::close() {
|
40
|
+
if (running_) {
|
41
|
+
running_ = false;
|
42
|
+
|
43
|
+
if (worker_thread_.joinable()) {
|
44
|
+
curl_multi_wakeup(multi_handle_);
|
45
|
+
worker_thread_.join();
|
46
|
+
}
|
47
|
+
|
48
|
+
cleanup();
|
49
|
+
|
50
|
+
curl_multi_cleanup(multi_handle_);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
py_object RedC::request(const char *method, const char *url, const char *raw_data, const py_object &data,
|
55
|
+
const py_object &files, const py_object &headers, const long &timeout_ms,
|
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) {
|
59
|
+
CHECK_RUNNING();
|
60
|
+
|
61
|
+
if (isNullOrEmpty(method) || isNullOrEmpty(url)) {
|
62
|
+
throw std::invalid_argument("method or url must be non-empty");
|
63
|
+
}
|
64
|
+
|
65
|
+
CURL *easy = curl_easy_init();
|
66
|
+
if (!easy) {
|
67
|
+
throw std::runtime_error("Failed to create CURL easy handle");
|
68
|
+
}
|
69
|
+
|
70
|
+
bool is_nobody = (strcmp(method, "HEAD") == 0 || strcmp(method, "OPTIONS") == 0);
|
71
|
+
|
72
|
+
try {
|
73
|
+
curl_easy_setopt(easy, CURLOPT_BUFFERSIZE, buffer_size_);
|
74
|
+
curl_easy_setopt(easy, CURLOPT_URL, url);
|
75
|
+
curl_easy_setopt(easy, CURLOPT_CUSTOMREQUEST, method);
|
76
|
+
curl_easy_setopt(easy, CURLOPT_NOSIGNAL, 1L);
|
77
|
+
|
78
|
+
curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, timeout_ms);
|
79
|
+
|
80
|
+
curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &RedC::header_callback);
|
81
|
+
|
82
|
+
if (verbose) {
|
83
|
+
curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
|
84
|
+
}
|
85
|
+
|
86
|
+
if (connect_timeout_ms > 0) {
|
87
|
+
curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, connect_timeout_ms);
|
88
|
+
}
|
89
|
+
|
90
|
+
if (is_nobody) {
|
91
|
+
curl_easy_setopt(easy, CURLOPT_NOBODY, 1L);
|
92
|
+
} else {
|
93
|
+
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &RedC::write_callback);
|
94
|
+
}
|
95
|
+
|
96
|
+
if (allow_redirect) {
|
97
|
+
curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L);
|
98
|
+
curl_easy_setopt(easy, CURLOPT_MAXREDIRS, 30L);
|
99
|
+
}
|
100
|
+
|
101
|
+
if (!isNullOrEmpty(proxy_url)) {
|
102
|
+
curl_easy_setopt(easy, CURLOPT_PROXY, proxy_url);
|
103
|
+
}
|
104
|
+
|
105
|
+
if (!verify) {
|
106
|
+
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0);
|
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);
|
110
|
+
}
|
111
|
+
|
112
|
+
CurlMime curl_mime_;
|
113
|
+
if (!isNullOrEmpty(raw_data)) {
|
114
|
+
curl_easy_setopt(easy, CURLOPT_POSTFIELDS, raw_data);
|
115
|
+
curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)strlen(raw_data));
|
116
|
+
} else if (!data.is_none() || !files.is_none()) {
|
117
|
+
curl_mime_.mime = curl_mime_init(easy);
|
118
|
+
|
119
|
+
if (!data.is_none()) {
|
120
|
+
dict dict_obj;
|
121
|
+
try {
|
122
|
+
dict_obj = nb::cast<dict>(data);
|
123
|
+
} catch (...) {
|
124
|
+
throw std::runtime_error("Expected \"data\" to be a dictionary of strings");
|
125
|
+
}
|
126
|
+
|
127
|
+
for (auto const &it : dict_obj) {
|
128
|
+
curl_mimepart *part = curl_mime_addpart(curl_mime_.mime);
|
129
|
+
curl_mime_name(part, nb::str(it.first).c_str());
|
130
|
+
curl_mime_data(part, nb::str(it.second).c_str(), CURL_ZERO_TERMINATED);
|
131
|
+
}
|
132
|
+
}
|
133
|
+
|
134
|
+
if (!files.is_none()) {
|
135
|
+
dict dict_obj;
|
136
|
+
try {
|
137
|
+
dict_obj = nb::cast<dict>(files);
|
138
|
+
} catch (...) {
|
139
|
+
throw std::runtime_error("Expected \"files\" to be a dictionary of strings");
|
140
|
+
}
|
141
|
+
|
142
|
+
for (auto const &it : dict_obj) {
|
143
|
+
curl_mimepart *part = curl_mime_addpart(curl_mime_.mime);
|
144
|
+
curl_mime_name(part, nb::str(it.first).c_str());
|
145
|
+
curl_mime_filedata(part, nb::str(it.second).c_str());
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
curl_easy_setopt(easy, CURLOPT_MIMEPOST, curl_mime_.mime);
|
150
|
+
}
|
151
|
+
|
152
|
+
CurlSlist slist_headers;
|
153
|
+
if (!headers.is_none()) {
|
154
|
+
for (auto const &it : headers) {
|
155
|
+
slist_headers.slist = curl_slist_append(slist_headers.slist, nb::str(it).c_str());
|
156
|
+
}
|
157
|
+
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, slist_headers.slist);
|
158
|
+
}
|
159
|
+
|
160
|
+
py_object future;
|
161
|
+
{
|
162
|
+
acq_gil gil;
|
163
|
+
future = loop_.attr("create_future")();
|
164
|
+
}
|
165
|
+
|
166
|
+
{
|
167
|
+
std::unique_lock<std::mutex> lock(mutex_);
|
168
|
+
auto &d = transfers_[easy];
|
169
|
+
lock.unlock();
|
170
|
+
|
171
|
+
d.future = future;
|
172
|
+
d.request_headers = std::move(slist_headers);
|
173
|
+
d.curl_mime_ = std::move(curl_mime_);
|
174
|
+
|
175
|
+
curl_easy_setopt(easy, CURLOPT_HEADERDATA, &d);
|
176
|
+
if (!is_nobody) {
|
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
|
+
}
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
queue_.enqueue(easy);
|
198
|
+
|
199
|
+
curl_multi_wakeup(multi_handle_); // thread-safe
|
200
|
+
return future;
|
201
|
+
} catch (...) {
|
202
|
+
curl_easy_cleanup(easy);
|
203
|
+
throw;
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
void RedC::worker_loop() {
|
208
|
+
while (running_) {
|
209
|
+
CURL *e;
|
210
|
+
if (queue_.try_dequeue(e)) {
|
211
|
+
CURLMcode res = curl_multi_add_handle(multi_handle_, e);
|
212
|
+
if (res != CURLM_OK) {
|
213
|
+
std::unique_lock<std::mutex> lock(mutex_);
|
214
|
+
auto it = transfers_.find(e);
|
215
|
+
if (it != transfers_.end()) {
|
216
|
+
Data data = std::move(it->second);
|
217
|
+
transfers_.erase(it);
|
218
|
+
lock.unlock();
|
219
|
+
{
|
220
|
+
acq_gil gil;
|
221
|
+
call_soon_threadsafe_(nb::cpp_function([data = std::move(data), res]() {
|
222
|
+
data.future.attr("set_result")(nb::make_tuple(-1, NULL, NULL, (int)res, curl_multi_strerror(res)));
|
223
|
+
}));
|
224
|
+
}
|
225
|
+
}
|
226
|
+
curl_easy_cleanup(e);
|
227
|
+
}
|
228
|
+
} else {
|
229
|
+
int numfds;
|
230
|
+
curl_multi_poll(multi_handle_, nullptr, 0, 30000, &numfds);
|
231
|
+
}
|
232
|
+
|
233
|
+
if (!running_) {
|
234
|
+
return;
|
235
|
+
}
|
236
|
+
|
237
|
+
curl_multi_perform(multi_handle_, &still_running_);
|
238
|
+
|
239
|
+
CURLMsg *msg;
|
240
|
+
int msgs_left;
|
241
|
+
while ((msg = curl_multi_info_read(multi_handle_, &msgs_left))) {
|
242
|
+
if (msg->msg == CURLMSG_DONE) {
|
243
|
+
std::unique_lock<std::mutex> lock(mutex_);
|
244
|
+
auto it = transfers_.find(msg->easy_handle);
|
245
|
+
if (it != transfers_.end()) {
|
246
|
+
Data data = std::move(it->second);
|
247
|
+
transfers_.erase(it);
|
248
|
+
lock.unlock();
|
249
|
+
|
250
|
+
{
|
251
|
+
acq_gil gil;
|
252
|
+
|
253
|
+
CURLcode res = msg->data.result;
|
254
|
+
|
255
|
+
/*
|
256
|
+
* Result is allways Tuple:
|
257
|
+
|
258
|
+
* 0: HTTP response status code.
|
259
|
+
* If the value is -1, it indicates a cURL error occurred
|
260
|
+
*
|
261
|
+
* 1: Response headers as bytes; can be null
|
262
|
+
*
|
263
|
+
* 2: The actual response data as bytes; can be null
|
264
|
+
*
|
265
|
+
* 3: cURL return code. This indicates the result code of the cURL operation.
|
266
|
+
* See: https://curl.se/libcurl/c/libcurl-errors.html
|
267
|
+
*
|
268
|
+
* 4: cURL error message string; can be null
|
269
|
+
*/
|
270
|
+
py_object result;
|
271
|
+
if (res == CURLE_OK) {
|
272
|
+
short status_code = 0;
|
273
|
+
curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &status_code);
|
274
|
+
result = nb::make_tuple(status_code, py_bytes(data.headers.data(), data.headers.size()),
|
275
|
+
py_bytes(data.response.data(), data.response.size()), (int)res, NULL);
|
276
|
+
} else {
|
277
|
+
result = nb::make_tuple(-1, NULL, NULL, (int)res, curl_easy_strerror(res));
|
278
|
+
}
|
279
|
+
|
280
|
+
call_soon_threadsafe_(nb::cpp_function([data = std::move(data), result = std::move(result)]() {
|
281
|
+
data.future.attr("set_result")(std::move(result));
|
282
|
+
}));
|
283
|
+
}
|
284
|
+
|
285
|
+
curl_multi_remove_handle(multi_handle_, msg->easy_handle);
|
286
|
+
curl_easy_cleanup(msg->easy_handle);
|
287
|
+
}
|
288
|
+
}
|
289
|
+
}
|
290
|
+
}
|
291
|
+
}
|
292
|
+
|
293
|
+
void RedC::cleanup() {
|
294
|
+
std::unique_lock<std::mutex> lock(mutex_);
|
295
|
+
for (auto &[easy, data] : transfers_) {
|
296
|
+
{
|
297
|
+
acq_gil gil;
|
298
|
+
call_soon_threadsafe_(data.future.attr("cancel"));
|
299
|
+
}
|
300
|
+
|
301
|
+
curl_multi_remove_handle(multi_handle_, easy);
|
302
|
+
curl_easy_cleanup(easy);
|
303
|
+
}
|
304
|
+
transfers_.clear();
|
305
|
+
}
|
306
|
+
|
307
|
+
void RedC::CHECK_RUNNING() {
|
308
|
+
if (!running_) {
|
309
|
+
throw std::runtime_error("RedC can't be used after being closed");
|
310
|
+
}
|
311
|
+
}
|
312
|
+
|
313
|
+
size_t RedC::header_callback(char *buffer, size_t size, size_t nitems, Data *clientp) {
|
314
|
+
size_t total_size = size * nitems;
|
315
|
+
clientp->headers.insert(clientp->headers.end(), buffer, buffer + total_size);
|
316
|
+
|
317
|
+
return total_size;
|
318
|
+
}
|
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
|
+
|
335
|
+
size_t RedC::write_callback(char *data, size_t size, size_t nmemb, Data *clientp) {
|
336
|
+
size_t total_size = size * nmemb;
|
337
|
+
|
338
|
+
if (clientp->has_stream_callback) {
|
339
|
+
try {
|
340
|
+
acq_gil gil;
|
341
|
+
clientp->stream_callback(py_bytes(data, total_size), total_size);
|
342
|
+
} catch (const std::exception &e) {
|
343
|
+
std::cerr << "Error in stream_callback: " << e.what() << std::endl;
|
344
|
+
}
|
345
|
+
} else {
|
346
|
+
clientp->response.insert(clientp->response.end(), data, data + total_size);
|
347
|
+
}
|
348
|
+
|
349
|
+
return total_size;
|
350
|
+
}
|
351
|
+
|
352
|
+
NB_MODULE(redc_ext, m) {
|
353
|
+
nb::class_<RedC>(m, "RedC")
|
354
|
+
.def(nb::init<const long &>())
|
355
|
+
.def("is_running", &RedC::is_running)
|
356
|
+
.def("request", &RedC::request, arg("method"), arg("url"), arg("raw_data") = "", arg("data") = nb::none(),
|
357
|
+
arg("files") = nb::none(), arg("headers") = nb::none(), arg("timeout_ms") = 60 * 1000,
|
358
|
+
arg("connect_timeout_ms") = 0, arg("allow_redirect") = true, arg("proxy_url") = "", arg("verify") = true,
|
359
|
+
arg("ca_cert_path") = "", arg("stream_callback") = nb::none(), arg("progress_callback") = nb::none(),
|
360
|
+
arg("verbose") = false)
|
361
|
+
.def("close", &RedC::close);
|
362
|
+
}
|
redc/ext/redc.h
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
#ifndef REDC_H
|
2
|
+
#define REDC_H
|
3
|
+
|
4
|
+
#include <atomic>
|
5
|
+
#include <cstring>
|
6
|
+
#include <map>
|
7
|
+
#include <mutex>
|
8
|
+
#include <thread>
|
9
|
+
#include <vector>
|
10
|
+
|
11
|
+
#include <curl/curl.h>
|
12
|
+
|
13
|
+
#include <nanobind/nanobind.h>
|
14
|
+
#include <nanobind/stl/string.h>
|
15
|
+
#include <nanobind/stl/tuple.h>
|
16
|
+
|
17
|
+
#include "utils/concurrentqueue.h"
|
18
|
+
#include "utils/curl_utils.h"
|
19
|
+
|
20
|
+
namespace nb = nanobind;
|
21
|
+
using namespace nb::literals;
|
22
|
+
|
23
|
+
using acq_gil = nb::gil_scoped_acquire;
|
24
|
+
using rel_gil = nb::gil_scoped_release;
|
25
|
+
|
26
|
+
using py_object = nb::object;
|
27
|
+
using py_bytes = nb::bytes;
|
28
|
+
using arg = nb::arg;
|
29
|
+
using dict = nb::dict;
|
30
|
+
|
31
|
+
bool isNullOrEmpty(const char *str) {
|
32
|
+
return !str || !*str;
|
33
|
+
}
|
34
|
+
|
35
|
+
struct Data {
|
36
|
+
py_object future;
|
37
|
+
py_object loop;
|
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};
|
43
|
+
|
44
|
+
std::vector<char> headers;
|
45
|
+
CurlSlist request_headers;
|
46
|
+
CurlMime curl_mime_;
|
47
|
+
|
48
|
+
std::vector<char> response;
|
49
|
+
};
|
50
|
+
|
51
|
+
class RedC {
|
52
|
+
public:
|
53
|
+
RedC(const long &buffer = 16384);
|
54
|
+
~RedC();
|
55
|
+
|
56
|
+
bool is_running();
|
57
|
+
void close();
|
58
|
+
|
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);
|
65
|
+
|
66
|
+
private:
|
67
|
+
int still_running_{0};
|
68
|
+
long buffer_size_;
|
69
|
+
py_object loop_;
|
70
|
+
py_object call_soon_threadsafe_;
|
71
|
+
|
72
|
+
CURLM *multi_handle_;
|
73
|
+
|
74
|
+
std::map<CURL *, Data> transfers_;
|
75
|
+
std::mutex mutex_;
|
76
|
+
std::thread worker_thread_;
|
77
|
+
std::atomic<bool> running_{false};
|
78
|
+
|
79
|
+
moodycamel::ConcurrentQueue<CURL *> queue_;
|
80
|
+
|
81
|
+
void worker_loop();
|
82
|
+
void cleanup();
|
83
|
+
void CHECK_RUNNING();
|
84
|
+
|
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);
|
88
|
+
static size_t write_callback(char *data, size_t size, size_t nmemb, Data *clientp);
|
89
|
+
};
|
90
|
+
|
91
|
+
#endif // REDC_H
|