1use std::{
17 collections::{HashMap, hash_map::DefaultHasher},
18 fs::File,
19 hash::{Hash, Hasher},
20 io::copy,
21 path::Path,
22 time::Duration,
23};
24
25use bytes::Bytes;
26use nautilus_core::{collections::into_ustr_vec, python::to_pyvalue_err};
27use pyo3::{create_exception, exceptions::PyException, prelude::*, types::PyDict};
28use reqwest::blocking::Client;
29
30use crate::{
31 http::{HttpClient, HttpClientError, HttpMethod, HttpResponse, HttpStatus},
32 ratelimiter::quota::Quota,
33};
34
35create_exception!(network, HttpError, PyException);
37
38create_exception!(network, HttpTimeoutError, PyException);
40
41create_exception!(network, HttpInvalidProxyError, PyException);
43
44create_exception!(network, HttpClientBuildError, PyException);
46
47impl HttpClientError {
48 #[must_use]
49 pub fn into_py_err(self) -> PyErr {
50 match self {
51 Self::Error(e) => PyErr::new::<HttpError, _>(e),
52 Self::TimeoutError(e) => PyErr::new::<HttpTimeoutError, _>(e),
53 Self::InvalidProxy(e) => PyErr::new::<HttpInvalidProxyError, _>(e),
54 Self::ClientBuildError(e) => PyErr::new::<HttpClientBuildError, _>(e),
55 }
56 }
57}
58
59#[pymethods]
60impl HttpMethod {
61 fn __hash__(&self) -> isize {
62 let mut h = DefaultHasher::new();
63 self.hash(&mut h);
64 h.finish() as isize
65 }
66}
67
68#[pymethods]
69impl HttpResponse {
70 #[new]
76 pub fn py_new(status: u16, body: Vec<u8>) -> PyResult<Self> {
77 Ok(Self {
78 status: HttpStatus::try_from(status).map_err(to_pyvalue_err)?,
79 headers: HashMap::new(),
80 body: Bytes::from(body),
81 })
82 }
83
84 #[getter]
85 #[pyo3(name = "status")]
86 pub const fn py_status(&self) -> u16 {
87 self.status.as_u16()
88 }
89
90 #[getter]
91 #[pyo3(name = "headers")]
92 pub fn py_headers(&self) -> HashMap<String, String> {
93 self.headers.clone()
94 }
95
96 #[getter]
97 #[pyo3(name = "body")]
98 pub fn py_body(&self) -> &[u8] {
99 self.body.as_ref()
100 }
101}
102
103#[pymethods]
104impl HttpClient {
105 #[new]
127 #[pyo3(signature = (default_headers=HashMap::new(), header_keys=Vec::new(), keyed_quotas=Vec::new(), default_quota=None, timeout_secs=None, proxy_url=None))]
128 pub fn py_new(
129 default_headers: HashMap<String, String>,
130 header_keys: Vec<String>,
131 keyed_quotas: Vec<(String, Quota)>,
132 default_quota: Option<Quota>,
133 timeout_secs: Option<u64>,
134 proxy_url: Option<String>,
135 ) -> PyResult<Self> {
136 Self::new(
137 default_headers,
138 header_keys,
139 keyed_quotas,
140 default_quota,
141 timeout_secs,
142 proxy_url,
143 )
144 .map_err(HttpClientError::into_py_err)
145 }
146
147 #[allow(clippy::too_many_arguments)]
148 #[pyo3(name = "request")]
149 #[pyo3(signature = (method, url, params=None, headers=None, body=None, keys=None, timeout_secs=None))]
150 fn py_request<'py>(
151 &self,
152 method: HttpMethod,
153 url: String,
154 params: Option<&Bound<'_, PyAny>>,
155 headers: Option<HashMap<String, String>>,
156 body: Option<Vec<u8>>,
157 keys: Option<Vec<String>>,
158 timeout_secs: Option<u64>,
159 py: Python<'py>,
160 ) -> PyResult<Bound<'py, PyAny>> {
161 let client = self.client.clone();
162 let rate_limiter = self.rate_limiter.clone();
163 let params = params_to_hashmap(params)?;
164
165 pyo3_async_runtimes::tokio::future_into_py(py, async move {
166 let keys = keys.map(into_ustr_vec);
167 rate_limiter.await_keys_ready(keys).await;
168 client
169 .send_request(
170 method.into(),
171 url,
172 params.as_ref(),
173 headers,
174 body,
175 timeout_secs,
176 )
177 .await
178 .map_err(HttpClientError::into_py_err)
179 })
180 }
181
182 #[pyo3(name = "get")]
183 #[pyo3(signature = (url, params=None, headers=None, keys=None, timeout_secs=None))]
184 fn py_get<'py>(
185 &self,
186 url: String,
187 params: Option<&Bound<'_, PyAny>>,
188 headers: Option<HashMap<String, String>>,
189 keys: Option<Vec<String>>,
190 timeout_secs: Option<u64>,
191 py: Python<'py>,
192 ) -> PyResult<Bound<'py, PyAny>> {
193 let client = self.clone();
194 let params = params_to_hashmap(params)?;
195 pyo3_async_runtimes::tokio::future_into_py(py, async move {
196 client
197 .get(url, params.as_ref(), headers, timeout_secs, keys)
198 .await
199 .map_err(HttpClientError::into_py_err)
200 })
201 }
202
203 #[allow(clippy::too_many_arguments)]
204 #[pyo3(name = "post")]
205 #[pyo3(signature = (url, params=None, headers=None, body=None, keys=None, timeout_secs=None))]
206 fn py_post<'py>(
207 &self,
208 url: String,
209 params: Option<&Bound<'_, PyAny>>,
210 headers: Option<HashMap<String, String>>,
211 body: Option<Vec<u8>>,
212 keys: Option<Vec<String>>,
213 timeout_secs: Option<u64>,
214 py: Python<'py>,
215 ) -> PyResult<Bound<'py, PyAny>> {
216 let client = self.clone();
217 let params = params_to_hashmap(params)?;
218 pyo3_async_runtimes::tokio::future_into_py(py, async move {
219 client
220 .post(url, params.as_ref(), headers, body, timeout_secs, keys)
221 .await
222 .map_err(HttpClientError::into_py_err)
223 })
224 }
225
226 #[allow(clippy::too_many_arguments)]
227 #[pyo3(name = "patch")]
228 #[pyo3(signature = (url, params=None, headers=None, body=None, keys=None, timeout_secs=None))]
229 fn py_patch<'py>(
230 &self,
231 url: String,
232 params: Option<&Bound<'_, PyAny>>,
233 headers: Option<HashMap<String, String>>,
234 body: Option<Vec<u8>>,
235 keys: Option<Vec<String>>,
236 timeout_secs: Option<u64>,
237 py: Python<'py>,
238 ) -> PyResult<Bound<'py, PyAny>> {
239 let client = self.clone();
240 let params = params_to_hashmap(params)?;
241 pyo3_async_runtimes::tokio::future_into_py(py, async move {
242 client
243 .patch(url, params.as_ref(), headers, body, timeout_secs, keys)
244 .await
245 .map_err(HttpClientError::into_py_err)
246 })
247 }
248
249 #[pyo3(name = "delete")]
250 #[pyo3(signature = (url, params=None, headers=None, keys=None, timeout_secs=None))]
251 fn py_delete<'py>(
252 &self,
253 url: String,
254 params: Option<&Bound<'_, PyAny>>,
255 headers: Option<HashMap<String, String>>,
256 keys: Option<Vec<String>>,
257 timeout_secs: Option<u64>,
258 py: Python<'py>,
259 ) -> PyResult<Bound<'py, PyAny>> {
260 let client = self.clone();
261 let params = params_to_hashmap(params)?;
262 pyo3_async_runtimes::tokio::future_into_py(py, async move {
263 client
264 .delete(url, params.as_ref(), headers, timeout_secs, keys)
265 .await
266 .map_err(HttpClientError::into_py_err)
267 })
268 }
269}
270
271fn params_to_hashmap(
277 params: Option<&Bound<'_, PyAny>>,
278) -> PyResult<Option<HashMap<String, Vec<String>>>> {
279 let Some(params) = params else {
280 return Ok(None);
281 };
282
283 let Ok(dict) = params.cast::<PyDict>() else {
284 return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
285 "params must be a dict",
286 ));
287 };
288
289 let mut result = HashMap::new();
290
291 for (key, value) in dict {
292 let key_str = key.str()?.to_str()?.to_string();
293
294 if let Ok(seq) = value.cast::<pyo3::types::PySequence>() {
295 if !value.is_instance_of::<pyo3::types::PyString>() {
297 let values: Vec<String> = (0..seq.len()?)
298 .map(|i| {
299 let item = seq.get_item(i)?;
300 Ok(item.str()?.to_str()?.to_string())
301 })
302 .collect::<PyResult<_>>()?;
303 result.insert(key_str, values);
304 continue;
305 }
306 }
307
308 let value_str = value.str()?.to_str()?.to_string();
309 result.insert(key_str, vec![value_str]);
310 }
311
312 Ok(Some(result))
313}
314
315#[pyfunction]
331#[pyo3(signature = (url, params=None, headers=None, timeout_secs=None))]
332pub fn http_get(
333 _py: Python<'_>,
334 url: String,
335 params: Option<&Bound<'_, PyAny>>,
336 headers: Option<HashMap<String, String>>,
337 timeout_secs: Option<u64>,
338) -> PyResult<HttpResponse> {
339 let params_map = params_to_hashmap(params)?;
340
341 std::thread::spawn(move || {
342 let runtime = tokio::runtime::Builder::new_current_thread()
343 .enable_all()
344 .build()
345 .expect("Failed to create runtime");
346
347 runtime.block_on(async {
348 let client = HttpClient::new(HashMap::new(), vec![], vec![], None, timeout_secs, None)
349 .map_err(HttpClientError::into_py_err)?;
350
351 client
352 .get(url, params_map.as_ref(), headers, timeout_secs, None)
353 .await
354 .map_err(HttpClientError::into_py_err)
355 })
356 })
357 .join()
358 .expect("Thread panicked")
359}
360
361#[pyfunction]
377#[pyo3(signature = (url, params=None, headers=None, body=None, timeout_secs=None))]
378pub fn http_post(
379 _py: Python<'_>,
380 url: String,
381 params: Option<&Bound<'_, PyAny>>,
382 headers: Option<HashMap<String, String>>,
383 body: Option<Vec<u8>>,
384 timeout_secs: Option<u64>,
385) -> PyResult<HttpResponse> {
386 let params_map = params_to_hashmap(params)?;
387
388 std::thread::spawn(move || {
389 let runtime = tokio::runtime::Builder::new_current_thread()
390 .enable_all()
391 .build()
392 .expect("Failed to create runtime");
393
394 runtime.block_on(async {
395 let client = HttpClient::new(HashMap::new(), vec![], vec![], None, timeout_secs, None)
396 .map_err(HttpClientError::into_py_err)?;
397
398 client
399 .post(url, params_map.as_ref(), headers, body, timeout_secs, None)
400 .await
401 .map_err(HttpClientError::into_py_err)
402 })
403 })
404 .join()
405 .expect("Thread panicked")
406}
407
408#[pyfunction]
424#[pyo3(signature = (url, params=None, headers=None, body=None, timeout_secs=None))]
425pub fn http_patch(
426 _py: Python<'_>,
427 url: String,
428 params: Option<&Bound<'_, PyAny>>,
429 headers: Option<HashMap<String, String>>,
430 body: Option<Vec<u8>>,
431 timeout_secs: Option<u64>,
432) -> PyResult<HttpResponse> {
433 let params_map = params_to_hashmap(params)?;
434
435 std::thread::spawn(move || {
436 let runtime = tokio::runtime::Builder::new_current_thread()
437 .enable_all()
438 .build()
439 .expect("Failed to create runtime");
440
441 runtime.block_on(async {
442 let client = HttpClient::new(HashMap::new(), vec![], vec![], None, timeout_secs, None)
443 .map_err(HttpClientError::into_py_err)?;
444
445 client
446 .patch(url, params_map.as_ref(), headers, body, timeout_secs, None)
447 .await
448 .map_err(HttpClientError::into_py_err)
449 })
450 })
451 .join()
452 .expect("Thread panicked")
453}
454
455#[pyfunction]
471#[pyo3(signature = (url, params=None, headers=None, timeout_secs=None))]
472pub fn http_delete(
473 _py: Python<'_>,
474 url: String,
475 params: Option<&Bound<'_, PyAny>>,
476 headers: Option<HashMap<String, String>>,
477 timeout_secs: Option<u64>,
478) -> PyResult<HttpResponse> {
479 let params_map = params_to_hashmap(params)?;
480
481 std::thread::spawn(move || {
482 let runtime = tokio::runtime::Builder::new_current_thread()
483 .enable_all()
484 .build()
485 .expect("Failed to create runtime");
486
487 runtime.block_on(async {
488 let client = HttpClient::new(HashMap::new(), vec![], vec![], None, timeout_secs, None)
489 .map_err(HttpClientError::into_py_err)?;
490
491 client
492 .delete(url, params_map.as_ref(), headers, timeout_secs, None)
493 .await
494 .map_err(HttpClientError::into_py_err)
495 })
496 })
497 .join()
498 .expect("Thread panicked")
499}
500
501#[pyfunction]
516#[pyo3(signature = (url, filepath, params=None, headers=None, timeout_secs=None))]
517pub fn http_download(
518 _py: Python<'_>,
519 url: String,
520 filepath: String,
521 params: Option<&Bound<'_, PyAny>>,
522 headers: Option<HashMap<String, String>>,
523 timeout_secs: Option<u64>,
524) -> PyResult<()> {
525 let params_map = params_to_hashmap(params)?;
526
527 let full_url = if let Some(ref params) = params_map {
529 let pairs: Vec<(String, String)> = params
531 .iter()
532 .flat_map(|(key, values)| values.iter().map(move |value| (key.clone(), value.clone())))
533 .collect();
534
535 if pairs.is_empty() {
536 url
537 } else {
538 let query_string = serde_urlencoded::to_string(pairs).map_err(to_pyvalue_err)?;
539 let separator = if url.contains('?') { '&' } else { '?' };
541 format!("{url}{separator}{query_string}")
542 }
543 } else {
544 url
545 };
546
547 let filepath = Path::new(&filepath);
548
549 if let Some(parent) = filepath.parent() {
550 std::fs::create_dir_all(parent).map_err(to_pyvalue_err)?;
551 }
552
553 let mut client_builder = Client::builder();
554 if let Some(timeout) = timeout_secs {
555 client_builder = client_builder.timeout(Duration::from_secs(timeout));
556 }
557 let client = client_builder.build().map_err(to_pyvalue_err)?;
558
559 let mut request_builder = client.get(&full_url);
560 if let Some(headers_map) = headers {
561 for (key, value) in headers_map {
562 request_builder = request_builder.header(key, value);
563 }
564 }
565
566 let mut response = request_builder.send().map_err(to_pyvalue_err)?;
567
568 if !response.status().is_success() {
569 return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
570 "HTTP error: {}",
571 response.status()
572 )));
573 }
574
575 let mut file = File::create(filepath).map_err(to_pyvalue_err)?;
576 copy(&mut response, &mut file).map_err(to_pyvalue_err)?;
577
578 Ok(())
579}
580
581#[cfg(test)]
582mod tests {
583 use std::net::{SocketAddr, TcpListener as StdTcpListener};
584
585 use axum::{Router, routing::get};
586 use pyo3::types::{PyDict, PyList, PyTuple};
587 use pyo3_async_runtimes::tokio::get_runtime;
588 use rstest::rstest;
589 use tokio::net::TcpListener;
590
591 use super::*;
592
593 #[rstest]
594 fn test_params_to_hashmap_none() {
595 pyo3::Python::initialize();
596
597 let result = Python::attach(|_py| params_to_hashmap(None)).unwrap();
598
599 assert!(result.is_none());
600 }
601
602 #[rstest]
603 fn test_params_to_hashmap_empty_dict() {
604 pyo3::Python::initialize();
605
606 let result = Python::attach(|py| {
607 let dict = PyDict::new(py);
608 params_to_hashmap(Some(dict.as_any()))
609 })
610 .unwrap();
611
612 assert!(result.is_some());
613 assert!(result.unwrap().is_empty());
614 }
615
616 #[rstest]
617 fn test_params_to_hashmap_single_string_value() {
618 pyo3::Python::initialize();
619
620 let result = Python::attach(|py| {
621 let dict = PyDict::new(py);
622 dict.set_item("key", "value").unwrap();
623 params_to_hashmap(Some(dict.as_any()))
624 })
625 .unwrap()
626 .unwrap();
627
628 assert_eq!(result.len(), 1);
629 assert_eq!(result.get("key").unwrap(), &vec!["value"]);
630 }
631
632 #[rstest]
633 fn test_params_to_hashmap_multiple_string_values() {
634 pyo3::Python::initialize();
635
636 let result = Python::attach(|py| {
637 let dict = PyDict::new(py);
638 dict.set_item("foo", "bar").unwrap();
639 dict.set_item("limit", "100").unwrap();
640 dict.set_item("offset", "0").unwrap();
641 params_to_hashmap(Some(dict.as_any()))
642 })
643 .unwrap()
644 .unwrap();
645
646 assert_eq!(result.len(), 3);
647 assert_eq!(result.get("foo").unwrap(), &vec!["bar"]);
648 assert_eq!(result.get("limit").unwrap(), &vec!["100"]);
649 assert_eq!(result.get("offset").unwrap(), &vec!["0"]);
650 }
651
652 #[rstest]
653 fn test_params_to_hashmap_int_value() {
654 pyo3::Python::initialize();
655
656 let result = Python::attach(|py| {
657 let dict = PyDict::new(py);
658 dict.set_item("limit", 100).unwrap();
659 params_to_hashmap(Some(dict.as_any()))
660 })
661 .unwrap()
662 .unwrap();
663
664 assert_eq!(result.len(), 1);
665 assert_eq!(result.get("limit").unwrap(), &vec!["100"]);
666 }
667
668 #[rstest]
669 fn test_params_to_hashmap_float_value() {
670 pyo3::Python::initialize();
671
672 let result = Python::attach(|py| {
673 let dict = PyDict::new(py);
674 dict.set_item("price", 123.45).unwrap();
675 params_to_hashmap(Some(dict.as_any()))
676 })
677 .unwrap()
678 .unwrap();
679
680 assert_eq!(result.len(), 1);
681 assert_eq!(result.get("price").unwrap(), &vec!["123.45"]);
682 }
683
684 #[rstest]
685 fn test_params_to_hashmap_bool_value() {
686 pyo3::Python::initialize();
687
688 let result = Python::attach(|py| {
689 let dict = PyDict::new(py);
690 dict.set_item("active", true).unwrap();
691 dict.set_item("closed", false).unwrap();
692 params_to_hashmap(Some(dict.as_any()))
693 })
694 .unwrap()
695 .unwrap();
696
697 assert_eq!(result.len(), 2);
698 assert_eq!(result.get("active").unwrap(), &vec!["True"]);
699 assert_eq!(result.get("closed").unwrap(), &vec!["False"]);
700 }
701
702 #[rstest]
703 fn test_params_to_hashmap_list_value() {
704 pyo3::Python::initialize();
705
706 let result = Python::attach(|py| {
707 let dict = PyDict::new(py);
708 let list = PyList::new(py, ["1", "2", "3"]).unwrap();
709 dict.set_item("id", list).unwrap();
710 params_to_hashmap(Some(dict.as_any()))
711 })
712 .unwrap()
713 .unwrap();
714
715 assert_eq!(result.len(), 1);
716 assert_eq!(result.get("id").unwrap(), &vec!["1", "2", "3"]);
717 }
718
719 #[rstest]
720 fn test_params_to_hashmap_tuple_value() {
721 pyo3::Python::initialize();
722
723 let result = Python::attach(|py| {
724 let dict = PyDict::new(py);
725 let tuple = PyTuple::new(py, ["a", "b", "c"]).unwrap();
726 dict.set_item("letters", tuple).unwrap();
727 params_to_hashmap(Some(dict.as_any()))
728 })
729 .unwrap()
730 .unwrap();
731
732 assert_eq!(result.len(), 1);
733 assert_eq!(result.get("letters").unwrap(), &vec!["a", "b", "c"]);
734 }
735
736 #[rstest]
737 fn test_params_to_hashmap_list_with_mixed_types() {
738 pyo3::Python::initialize();
739
740 let result = Python::attach(|py| {
741 let dict = PyDict::new(py);
742 let list = PyList::new(py, [1, 2, 3]).unwrap();
743 dict.set_item("nums", list).unwrap();
744 params_to_hashmap(Some(dict.as_any()))
745 })
746 .unwrap()
747 .unwrap();
748
749 assert_eq!(result.len(), 1);
750 assert_eq!(result.get("nums").unwrap(), &vec!["1", "2", "3"]);
751 }
752
753 #[rstest]
754 fn test_params_to_hashmap_mixed_values() {
755 pyo3::Python::initialize();
756
757 let result = Python::attach(|py| {
758 let dict = PyDict::new(py);
759 dict.set_item("name", "test").unwrap();
760 dict.set_item("limit", 50).unwrap();
761 let ids = PyList::new(py, ["1", "2"]).unwrap();
762 dict.set_item("id", ids).unwrap();
763 params_to_hashmap(Some(dict.as_any()))
764 })
765 .unwrap()
766 .unwrap();
767
768 assert_eq!(result.len(), 3);
769 assert_eq!(result.get("name").unwrap(), &vec!["test"]);
770 assert_eq!(result.get("limit").unwrap(), &vec!["50"]);
771 assert_eq!(result.get("id").unwrap(), &vec!["1", "2"]);
772 }
773
774 #[rstest]
775 fn test_params_to_hashmap_string_not_treated_as_sequence() {
776 pyo3::Python::initialize();
777
778 let result = Python::attach(|py| {
779 let dict = PyDict::new(py);
780 dict.set_item("text", "hello").unwrap();
781 params_to_hashmap(Some(dict.as_any()))
782 })
783 .unwrap()
784 .unwrap();
785
786 assert_eq!(result.len(), 1);
787 assert_eq!(result.get("text").unwrap(), &vec!["hello"]);
789 }
790
791 #[rstest]
792 fn test_params_to_hashmap_invalid_non_dict() {
793 pyo3::Python::initialize();
794
795 let result = Python::attach(|py| {
796 let list = PyList::new(py, ["a", "b"]).unwrap();
797 params_to_hashmap(Some(list.as_any()))
798 });
799
800 assert!(result.is_err());
801 let err = result.unwrap_err();
802 assert!(err.to_string().contains("params must be a dict"));
803 }
804
805 #[rstest]
806 fn test_params_to_hashmap_invalid_string_param() {
807 pyo3::Python::initialize();
808
809 let result = Python::attach(|py| {
810 let string = pyo3::types::PyString::new(py, "not a dict");
811 params_to_hashmap(Some(string.as_any()))
812 });
813
814 assert!(result.is_err());
815 let err = result.unwrap_err();
816 assert!(err.to_string().contains("params must be a dict"));
817 }
818
819 fn get_unique_port() -> u16 {
820 let listener =
821 StdTcpListener::bind("127.0.0.1:0").expect("Failed to bind temporary TcpListener");
822 listener.local_addr().unwrap().port()
823 }
824
825 async fn create_test_router() -> Router {
826 Router::new()
827 .route("/get", get(|| async { "hello-world!" }))
828 .route("/post", axum::routing::post(|| async { "posted" }))
829 .route("/patch", axum::routing::patch(|| async { "patched" }))
830 .route("/delete", axum::routing::delete(|| async { "deleted" }))
831 }
832
833 async fn start_test_server() -> Result<SocketAddr, Box<dyn std::error::Error + Send + Sync>> {
834 let port = get_unique_port();
835 let listener = TcpListener::bind(format!("127.0.0.1:{port}")).await?;
836 let addr = listener.local_addr()?;
837
838 tokio::spawn(async move {
839 let app = create_test_router().await;
840 axum::serve(listener, app).await.unwrap();
841 });
842
843 Ok(addr)
844 }
845
846 #[rstest]
847 fn test_blocking_http_get() {
848 pyo3::Python::initialize();
849
850 let addr = get_runtime().block_on(async { start_test_server().await.unwrap() });
851 let url = format!("http://{addr}/get");
852
853 let response = Python::attach(|py| http_get(py, url, None, None, Some(10))).unwrap();
854
855 assert!(response.status.is_success());
856 assert_eq!(String::from_utf8_lossy(&response.body), "hello-world!");
857 }
858
859 #[rstest]
860 fn test_blocking_http_post() {
861 pyo3::Python::initialize();
862
863 let addr = get_runtime().block_on(async { start_test_server().await.unwrap() });
864 let url = format!("http://{addr}/post");
865
866 let response = Python::attach(|py| http_post(py, url, None, None, None, Some(10))).unwrap();
867
868 assert!(response.status.is_success());
869 assert_eq!(String::from_utf8_lossy(&response.body), "posted");
870 }
871
872 #[rstest]
873 fn test_blocking_http_patch() {
874 pyo3::Python::initialize();
875
876 let addr = get_runtime().block_on(async { start_test_server().await.unwrap() });
877 let url = format!("http://{addr}/patch");
878
879 let response =
880 Python::attach(|py| http_patch(py, url, None, None, None, Some(10))).unwrap();
881
882 assert!(response.status.is_success());
883 assert_eq!(String::from_utf8_lossy(&response.body), "patched");
884 }
885
886 #[rstest]
887 fn test_blocking_http_delete() {
888 pyo3::Python::initialize();
889
890 let addr = get_runtime().block_on(async { start_test_server().await.unwrap() });
891 let url = format!("http://{addr}/delete");
892
893 let response = Python::attach(|py| http_delete(py, url, None, None, Some(10))).unwrap();
894
895 assert!(response.status.is_success());
896 assert_eq!(String::from_utf8_lossy(&response.body), "deleted");
897 }
898
899 #[rstest]
900 fn test_blocking_http_download() {
901 pyo3::Python::initialize();
902
903 let addr = get_runtime().block_on(async { start_test_server().await.unwrap() });
904 let url = format!("http://{addr}/get");
905 let temp_dir = std::env::temp_dir();
906 let filepath = temp_dir.join("test_download.txt");
907
908 Python::attach(|py| {
909 http_download(
910 py,
911 url,
912 filepath.to_str().unwrap().to_string(),
913 None,
914 None,
915 Some(10),
916 )
917 .unwrap();
918 });
919
920 assert!(filepath.exists());
921 let content = std::fs::read_to_string(&filepath).unwrap();
922 assert_eq!(content, "hello-world!");
923
924 std::fs::remove_file(&filepath).ok();
925 }
926}