nautilus_network/python/http.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3// https://nautechsystems.io
4//
5// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6// You may not use this file except in compliance with the License.
7// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{
17 collections::{hash_map::DefaultHasher, HashMap},
18 hash::{Hash, Hasher},
19};
20
21use bytes::Bytes;
22use nautilus_core::python::to_pyvalue_err;
23use pyo3::{create_exception, exceptions::PyException, prelude::*};
24
25use crate::{
26 http::{HttpClient, HttpClientError, HttpMethod, HttpResponse, HttpStatus},
27 ratelimiter::quota::Quota,
28};
29
30// Python exception class for generic HTTP errors.
31create_exception!(network, HttpError, PyException);
32
33// Python exception class for generic HTTP timeout errors.
34create_exception!(network, HttpTimeoutError, PyException);
35
36impl HttpClientError {
37 #[must_use]
38 pub fn into_py_err(self) -> PyErr {
39 match self {
40 Self::Error(e) => PyErr::new::<HttpError, _>(e),
41 Self::TimeoutError(e) => PyErr::new::<HttpTimeoutError, _>(e),
42 }
43 }
44}
45
46#[pymethods]
47impl HttpMethod {
48 fn __hash__(&self) -> isize {
49 let mut h = DefaultHasher::new();
50 self.hash(&mut h);
51 h.finish() as isize
52 }
53}
54
55#[pymethods]
56impl HttpResponse {
57 #[new]
58 pub fn py_new(status: u16, body: Vec<u8>) -> PyResult<Self> {
59 Ok(Self {
60 status: HttpStatus::from(status).map_err(to_pyvalue_err)?,
61 headers: HashMap::new(),
62 body: Bytes::from(body),
63 })
64 }
65
66 #[getter]
67 #[pyo3(name = "status")]
68 pub const fn py_status(&self) -> u16 {
69 self.status.as_u16()
70 }
71
72 #[getter]
73 #[pyo3(name = "headers")]
74 pub fn py_headers(&self) -> HashMap<String, String> {
75 self.headers.clone()
76 }
77
78 #[getter]
79 #[pyo3(name = "body")]
80 pub fn py_body(&self) -> &[u8] {
81 self.body.as_ref()
82 }
83}
84
85#[pymethods]
86impl HttpClient {
87 /// Creates a new HttpClient.
88 ///
89 /// `default_headers`: The default headers to be used with every request.
90 /// `header_keys`: The key value pairs for the given `header_keys` are retained from the responses.
91 /// `keyed_quota`: A list of string quota pairs that gives quota for specific key values.
92 /// `default_quota`: The default rate limiting quota for any request.
93 /// Default quota is optional and no quota is passthrough.
94 ///
95 /// Rate limiting can be configured on a per-endpoint basis by passing
96 /// key-value pairs of endpoint URLs and their respective quotas.
97 ///
98 /// For /foo -> 10 reqs/sec configure limit with ("foo", Quota.rate_per_second(10))
99 ///
100 /// Hierarchical rate limiting can be achieved by configuring the quotas for
101 /// each level.
102 ///
103 /// For /foo/bar -> 10 reqs/sec and /foo -> 20 reqs/sec configure limits for
104 /// keys "foo/bar" and "foo" respectively.
105 ///
106 /// When a request is made the URL should be split into all the keys within it.
107 ///
108 /// For request /foo/bar, should pass keys ["foo/bar", "foo"] for rate limiting.
109 #[new]
110 #[pyo3(signature = (default_headers = HashMap::new(), header_keys = Vec::new(), keyed_quotas = Vec::new(), default_quota = None))]
111 #[must_use]
112 pub fn py_new(
113 default_headers: HashMap<String, String>,
114 header_keys: Vec<String>,
115 keyed_quotas: Vec<(String, Quota)>,
116 default_quota: Option<Quota>,
117 ) -> Self {
118 Self::new(default_headers, header_keys, keyed_quotas, default_quota)
119 }
120
121 /// Sends an HTTP request.
122 ///
123 /// `method`: The HTTP method to call.
124 /// `url`: The request is sent to this url.
125 /// `headers`: The header key value pairs in the request.
126 /// `body`: The bytes sent in the body of request.
127 /// `keys`: The keys used for rate limiting the request.
128 ///
129 /// # Example
130 ///
131 /// When a request is made the URL should be split into all relevant keys within it.
132 ///
133 /// For request /foo/bar, should pass keys ["foo/bar", "foo"] for rate limiting.
134 #[pyo3(name = "request")]
135 #[allow(clippy::too_many_arguments)]
136 #[pyo3(signature = (method, url, headers=None, body=None, keys=None, timeout_secs=None))]
137 fn py_request<'py>(
138 &self,
139 method: HttpMethod,
140 url: String,
141 headers: Option<HashMap<String, String>>,
142 body: Option<Vec<u8>>,
143 keys: Option<Vec<String>>,
144 timeout_secs: Option<u64>,
145 py: Python<'py>,
146 ) -> PyResult<Bound<'py, PyAny>> {
147 let client = self.client.clone();
148 let rate_limiter = self.rate_limiter.clone();
149
150 pyo3_async_runtimes::tokio::future_into_py(py, async move {
151 rate_limiter.await_keys_ready(keys).await;
152 client
153 .send_request(method.into(), url, headers, body, timeout_secs)
154 .await
155 .map_err(HttpClientError::into_py_err)
156 })
157 }
158}