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}