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