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
556 if let Some(timeout) = timeout_secs {
557 client_builder = client_builder.timeout(Duration::from_secs(timeout));
558 }
559 let client = client_builder.build().map_err(to_pyvalue_err)?;
560
561 let mut request_builder = client.get(&full_url);
562
563 if let Some(headers_map) = headers {
564 for (key, value) in headers_map {
565 request_builder = request_builder.header(key, value);
566 }
567 }
568
569 let mut response = request_builder.send().map_err(to_pyvalue_err)?;
570
571 if !response.status().is_success() {
572 return Err(to_pyruntime_err(format!(
573 "HTTP error: {}",
574 response.status()
575 )));
576 }
577
578 let mut file = File::create(filepath).map_err(to_pyvalue_err)?;
579 copy(&mut response, &mut file).map_err(to_pyvalue_err)?;
580
581 Ok(())
582}
583
584#[cfg(test)]
585mod tests {
586 use std::net::SocketAddr;
587
588 use axum::{Router, routing::get};
589 use pyo3::types::{PyDict, PyList, PyTuple};
590 use pyo3_async_runtimes::tokio::get_runtime;
591 use rstest::rstest;
592 use tokio::net::TcpListener;
593
594 use super::*;
595
596 #[rstest]
597 fn test_params_to_hashmap_none() {
598 pyo3::Python::initialize();
599
600 let result = Python::attach(|_py| params_to_hashmap(None)).unwrap();
601
602 assert!(result.is_none());
603 }
604
605 #[rstest]
606 fn test_params_to_hashmap_empty_dict() {
607 pyo3::Python::initialize();
608
609 let result = Python::attach(|py| {
610 let dict = PyDict::new(py);
611 params_to_hashmap(Some(dict.as_any()))
612 })
613 .unwrap();
614
615 assert!(result.is_some());
616 assert!(result.unwrap().is_empty());
617 }
618
619 #[rstest]
620 fn test_params_to_hashmap_single_string_value() {
621 pyo3::Python::initialize();
622
623 let result = Python::attach(|py| {
624 let dict = PyDict::new(py);
625 dict.set_item("key", "value").unwrap();
626 params_to_hashmap(Some(dict.as_any()))
627 })
628 .unwrap()
629 .unwrap();
630
631 assert_eq!(result.len(), 1);
632 assert_eq!(result.get("key").unwrap(), &vec!["value"]);
633 }
634
635 #[rstest]
636 fn test_params_to_hashmap_multiple_string_values() {
637 pyo3::Python::initialize();
638
639 let result = Python::attach(|py| {
640 let dict = PyDict::new(py);
641 dict.set_item("foo", "bar").unwrap();
642 dict.set_item("limit", "100").unwrap();
643 dict.set_item("offset", "0").unwrap();
644 params_to_hashmap(Some(dict.as_any()))
645 })
646 .unwrap()
647 .unwrap();
648
649 assert_eq!(result.len(), 3);
650 assert_eq!(result.get("foo").unwrap(), &vec!["bar"]);
651 assert_eq!(result.get("limit").unwrap(), &vec!["100"]);
652 assert_eq!(result.get("offset").unwrap(), &vec!["0"]);
653 }
654
655 #[rstest]
656 fn test_params_to_hashmap_int_value() {
657 pyo3::Python::initialize();
658
659 let result = Python::attach(|py| {
660 let dict = PyDict::new(py);
661 dict.set_item("limit", 100).unwrap();
662 params_to_hashmap(Some(dict.as_any()))
663 })
664 .unwrap()
665 .unwrap();
666
667 assert_eq!(result.len(), 1);
668 assert_eq!(result.get("limit").unwrap(), &vec!["100"]);
669 }
670
671 #[rstest]
672 fn test_params_to_hashmap_float_value() {
673 pyo3::Python::initialize();
674
675 let result = Python::attach(|py| {
676 let dict = PyDict::new(py);
677 dict.set_item("price", 123.45).unwrap();
678 params_to_hashmap(Some(dict.as_any()))
679 })
680 .unwrap()
681 .unwrap();
682
683 assert_eq!(result.len(), 1);
684 assert_eq!(result.get("price").unwrap(), &vec!["123.45"]);
685 }
686
687 #[rstest]
688 fn test_params_to_hashmap_bool_value() {
689 pyo3::Python::initialize();
690
691 let result = Python::attach(|py| {
692 let dict = PyDict::new(py);
693 dict.set_item("active", true).unwrap();
694 dict.set_item("closed", false).unwrap();
695 params_to_hashmap(Some(dict.as_any()))
696 })
697 .unwrap()
698 .unwrap();
699
700 assert_eq!(result.len(), 2);
701 assert_eq!(result.get("active").unwrap(), &vec!["True"]);
702 assert_eq!(result.get("closed").unwrap(), &vec!["False"]);
703 }
704
705 #[rstest]
706 fn test_params_to_hashmap_list_value() {
707 pyo3::Python::initialize();
708
709 let result = Python::attach(|py| {
710 let dict = PyDict::new(py);
711 let list = PyList::new(py, ["1", "2", "3"]).unwrap();
712 dict.set_item("id", list).unwrap();
713 params_to_hashmap(Some(dict.as_any()))
714 })
715 .unwrap()
716 .unwrap();
717
718 assert_eq!(result.len(), 1);
719 assert_eq!(result.get("id").unwrap(), &vec!["1", "2", "3"]);
720 }
721
722 #[rstest]
723 fn test_params_to_hashmap_tuple_value() {
724 pyo3::Python::initialize();
725
726 let result = Python::attach(|py| {
727 let dict = PyDict::new(py);
728 let tuple = PyTuple::new(py, ["a", "b", "c"]).unwrap();
729 dict.set_item("letters", tuple).unwrap();
730 params_to_hashmap(Some(dict.as_any()))
731 })
732 .unwrap()
733 .unwrap();
734
735 assert_eq!(result.len(), 1);
736 assert_eq!(result.get("letters").unwrap(), &vec!["a", "b", "c"]);
737 }
738
739 #[rstest]
740 fn test_params_to_hashmap_list_with_mixed_types() {
741 pyo3::Python::initialize();
742
743 let result = Python::attach(|py| {
744 let dict = PyDict::new(py);
745 let list = PyList::new(py, [1, 2, 3]).unwrap();
746 dict.set_item("nums", list).unwrap();
747 params_to_hashmap(Some(dict.as_any()))
748 })
749 .unwrap()
750 .unwrap();
751
752 assert_eq!(result.len(), 1);
753 assert_eq!(result.get("nums").unwrap(), &vec!["1", "2", "3"]);
754 }
755
756 #[rstest]
757 fn test_params_to_hashmap_mixed_values() {
758 pyo3::Python::initialize();
759
760 let result = Python::attach(|py| {
761 let dict = PyDict::new(py);
762 dict.set_item("name", "test").unwrap();
763 dict.set_item("limit", 50).unwrap();
764 let ids = PyList::new(py, ["1", "2"]).unwrap();
765 dict.set_item("id", ids).unwrap();
766 params_to_hashmap(Some(dict.as_any()))
767 })
768 .unwrap()
769 .unwrap();
770
771 assert_eq!(result.len(), 3);
772 assert_eq!(result.get("name").unwrap(), &vec!["test"]);
773 assert_eq!(result.get("limit").unwrap(), &vec!["50"]);
774 assert_eq!(result.get("id").unwrap(), &vec!["1", "2"]);
775 }
776
777 #[rstest]
778 fn test_params_to_hashmap_string_not_treated_as_sequence() {
779 pyo3::Python::initialize();
780
781 let result = Python::attach(|py| {
782 let dict = PyDict::new(py);
783 dict.set_item("text", "hello").unwrap();
784 params_to_hashmap(Some(dict.as_any()))
785 })
786 .unwrap()
787 .unwrap();
788
789 assert_eq!(result.len(), 1);
790 assert_eq!(result.get("text").unwrap(), &vec!["hello"]);
792 }
793
794 #[rstest]
795 fn test_params_to_hashmap_invalid_non_dict() {
796 pyo3::Python::initialize();
797
798 let result = Python::attach(|py| {
799 let list = PyList::new(py, ["a", "b"]).unwrap();
800 params_to_hashmap(Some(list.as_any()))
801 });
802
803 assert!(result.is_err());
804 let err = result.unwrap_err();
805 assert!(err.to_string().contains("params must be a dict"));
806 }
807
808 #[rstest]
809 fn test_params_to_hashmap_invalid_string_param() {
810 pyo3::Python::initialize();
811
812 let result = Python::attach(|py| {
813 let string = pyo3::types::PyString::new(py, "not a dict");
814 params_to_hashmap(Some(string.as_any()))
815 });
816
817 assert!(result.is_err());
818 let err = result.unwrap_err();
819 assert!(err.to_string().contains("params must be a dict"));
820 }
821
822 async fn create_test_router() -> Router {
823 Router::new()
824 .route("/get", get(|| async { "hello-world!" }))
825 .route("/post", axum::routing::post(|| async { "posted" }))
826 .route("/patch", axum::routing::patch(|| async { "patched" }))
827 .route("/delete", axum::routing::delete(|| async { "deleted" }))
828 }
829
830 async fn start_test_server() -> Result<SocketAddr, Box<dyn std::error::Error + Send + Sync>> {
831 let listener = TcpListener::bind("127.0.0.1:0").await?;
832 let addr = listener.local_addr()?;
833
834 tokio::spawn(async move {
835 let app = create_test_router().await;
836 axum::serve(listener, app).await.unwrap();
837 });
838
839 Ok(addr)
840 }
841
842 #[rstest]
843 fn test_blocking_http_get() {
844 pyo3::Python::initialize();
845
846 let addr = get_runtime().block_on(async { start_test_server().await.unwrap() });
847 let url = format!("http://{addr}/get");
848
849 let response = Python::attach(|py| http_get(py, url, None, None, Some(10))).unwrap();
850
851 assert!(response.status.is_success());
852 assert_eq!(String::from_utf8_lossy(&response.body), "hello-world!");
853 }
854
855 #[rstest]
856 fn test_blocking_http_post() {
857 pyo3::Python::initialize();
858
859 let addr = get_runtime().block_on(async { start_test_server().await.unwrap() });
860 let url = format!("http://{addr}/post");
861
862 let response = Python::attach(|py| http_post(py, url, None, None, None, Some(10))).unwrap();
863
864 assert!(response.status.is_success());
865 assert_eq!(String::from_utf8_lossy(&response.body), "posted");
866 }
867
868 #[rstest]
869 fn test_blocking_http_patch() {
870 pyo3::Python::initialize();
871
872 let addr = get_runtime().block_on(async { start_test_server().await.unwrap() });
873 let url = format!("http://{addr}/patch");
874
875 let response =
876 Python::attach(|py| http_patch(py, url, None, None, None, Some(10))).unwrap();
877
878 assert!(response.status.is_success());
879 assert_eq!(String::from_utf8_lossy(&response.body), "patched");
880 }
881
882 #[rstest]
883 fn test_blocking_http_delete() {
884 pyo3::Python::initialize();
885
886 let addr = get_runtime().block_on(async { start_test_server().await.unwrap() });
887 let url = format!("http://{addr}/delete");
888
889 let response = Python::attach(|py| http_delete(py, url, None, None, Some(10))).unwrap();
890
891 assert!(response.status.is_success());
892 assert_eq!(String::from_utf8_lossy(&response.body), "deleted");
893 }
894
895 #[rstest]
896 fn test_blocking_http_download() {
897 pyo3::Python::initialize();
898
899 let addr = get_runtime().block_on(async { start_test_server().await.unwrap() });
900 let url = format!("http://{addr}/get");
901 let temp_dir = std::env::temp_dir();
902 let filepath = temp_dir.join("test_download.txt");
903
904 Python::attach(|py| {
905 http_download(
906 py,
907 url,
908 filepath.to_str().unwrap().to_string(),
909 None,
910 None,
911 Some(10),
912 )
913 .unwrap();
914 });
915
916 assert!(filepath.exists());
917 let content = std::fs::read_to_string(&filepath).unwrap();
918 assert_eq!(content, "hello-world!");
919
920 std::fs::remove_file(&filepath).ok();
921 }
922}