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::{HashMap, hash_map::DefaultHasher},
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    /// Creates a new [`HttpResponse`] instance.
58    ///
59    /// # Errors
60    ///
61    /// Returns an error for an invalid `status` code.
62    #[new]
63    pub fn py_new(status: u16, body: Vec<u8>) -> PyResult<Self> {
64        Ok(Self {
65            status: HttpStatus::from(status).map_err(to_pyvalue_err)?,
66            headers: HashMap::new(),
67            body: Bytes::from(body),
68        })
69    }
70
71    #[getter]
72    #[pyo3(name = "status")]
73    pub const fn py_status(&self) -> u16 {
74        self.status.as_u16()
75    }
76
77    #[getter]
78    #[pyo3(name = "headers")]
79    pub fn py_headers(&self) -> HashMap<String, String> {
80        self.headers.clone()
81    }
82
83    #[getter]
84    #[pyo3(name = "body")]
85    pub fn py_body(&self) -> &[u8] {
86        self.body.as_ref()
87    }
88}
89
90#[pymethods]
91impl HttpClient {
92    /// Creates a new HttpClient.
93    ///
94    /// `default_headers`: The default headers to be used with every request.
95    /// `header_keys`: The key value pairs for the given `header_keys` are retained from the responses.
96    /// `keyed_quota`: A list of string quota pairs that gives quota for specific key values.
97    /// `default_quota`: The default rate limiting quota for any request.
98    /// Default quota is optional and no quota is passthrough.
99    ///
100    /// Rate limiting can be configured on a per-endpoint basis by passing
101    /// key-value pairs of endpoint URLs and their respective quotas.
102    ///
103    /// For /foo -> 10 reqs/sec configure limit with ("foo", Quota.rate_per_second(10))
104    ///
105    /// Hierarchical rate limiting can be achieved by configuring the quotas for
106    /// each level.
107    ///
108    /// For /foo/bar -> 10 reqs/sec and /foo -> 20 reqs/sec configure limits for
109    /// keys "foo/bar" and "foo" respectively.
110    ///
111    /// When a request is made the URL should be split into all the keys within it.
112    ///
113    /// For request /foo/bar, should pass keys ["foo/bar", "foo"] for rate limiting.
114    #[new]
115    #[pyo3(signature = (default_headers = HashMap::new(), header_keys = Vec::new(), keyed_quotas = Vec::new(), default_quota = None))]
116    #[must_use]
117    pub fn py_new(
118        default_headers: HashMap<String, String>,
119        header_keys: Vec<String>,
120        keyed_quotas: Vec<(String, Quota)>,
121        default_quota: Option<Quota>,
122    ) -> Self {
123        Self::new(default_headers, header_keys, keyed_quotas, default_quota)
124    }
125
126    /// Sends an HTTP request.
127    ///
128    /// `method`: The HTTP method to call.
129    /// `url`: The request is sent to this url.
130    /// `headers`: The header key value pairs in the request.
131    /// `body`: The bytes sent in the body of request.
132    /// `keys`: The keys used for rate limiting the request.
133    ///
134    /// # Example
135    ///
136    /// When a request is made the URL should be split into all relevant keys within it.
137    ///
138    /// For request /foo/bar, should pass keys ["foo/bar", "foo"] for rate limiting.
139    #[pyo3(name = "request")]
140    #[allow(clippy::too_many_arguments)]
141    #[pyo3(signature = (method, url, headers=None, body=None, keys=None, timeout_secs=None))]
142    fn py_request<'py>(
143        &self,
144        method: HttpMethod,
145        url: String,
146        headers: Option<HashMap<String, String>>,
147        body: Option<Vec<u8>>,
148        keys: Option<Vec<String>>,
149        timeout_secs: Option<u64>,
150        py: Python<'py>,
151    ) -> PyResult<Bound<'py, PyAny>> {
152        let client = self.client.clone();
153        let rate_limiter = self.rate_limiter.clone();
154
155        pyo3_async_runtimes::tokio::future_into_py(py, async move {
156            rate_limiter.await_keys_ready(keys).await;
157            client
158                .send_request(method.into(), url, headers, body, timeout_secs)
159                .await
160                .map_err(HttpClientError::into_py_err)
161        })
162    }
163}