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}