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