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