1use nautilus_core::{python::to_pyvalue_err, string::secret::SecretString};
19use nautilus_model::identifiers::{AccountId, Venue};
20use nautilus_network::websocket::TransportBackend;
21use pyo3::{PyResult, pymethods};
22
23use crate::{
24 common::enums::{LighterDeployment, LighterEnvironment},
25 config::{
26 LighterDataClientConfig, LighterExecutionClientConfig, validate_scheduled_cancel_timeout,
27 },
28};
29
30#[pymethods]
31#[pyo3_stub_gen::derive::gen_stub_pymethods]
32impl LighterDataClientConfig {
33 #[new]
35 #[pyo3(signature = (
36 base_url_http = None,
37 base_url_ws = None,
38 proxy_url = None,
39 environment = None,
40 account_index = None,
41 api_key_index = None,
42 private_key = None,
43 http_timeout_secs = None,
44 ws_timeout_secs = None,
45 update_instruments_interval_mins = None,
46 rest_quota_per_min = None,
47 transport_backend = None,
48 deployment = None,
49 venue = None,
50 book_snapshot_timeout_secs = None,
51 ))]
52 #[expect(clippy::too_many_arguments)]
53 fn py_new(
54 base_url_http: Option<String>,
55 base_url_ws: Option<String>,
56 proxy_url: Option<String>,
57 environment: Option<LighterEnvironment>,
58 account_index: Option<u64>,
59 api_key_index: Option<u8>,
60 private_key: Option<String>,
61 http_timeout_secs: Option<u64>,
62 ws_timeout_secs: Option<u64>,
63 update_instruments_interval_mins: Option<u64>,
64 rest_quota_per_min: Option<u32>,
65 transport_backend: Option<TransportBackend>,
66 deployment: Option<LighterDeployment>,
67 venue: Option<Venue>,
68 book_snapshot_timeout_secs: Option<u64>,
69 ) -> Self {
70 let defaults = Self::default();
71 Self {
72 environment: environment.unwrap_or(defaults.environment),
73 deployment: deployment.unwrap_or(defaults.deployment),
74 venue,
75 account_index,
76 api_key_index,
77 private_key: private_key.map(SecretString::from),
78 base_url_http,
79 base_url_ws,
80 proxy_url: proxy_url.map(SecretString::from),
81 http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
82 ws_timeout_secs: ws_timeout_secs.unwrap_or(defaults.ws_timeout_secs),
83 update_instruments_interval_mins: update_instruments_interval_mins
84 .unwrap_or(defaults.update_instruments_interval_mins),
85 book_snapshot_timeout_secs: book_snapshot_timeout_secs
86 .unwrap_or(defaults.book_snapshot_timeout_secs),
87 rest_quota_per_min,
88 transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
89 }
90 }
91
92 #[getter]
93 const fn has_proxy_url(&self) -> bool {
94 self.proxy_url.is_some()
95 }
96
97 fn __repr__(&self) -> String {
98 stringify!(LighterDataClientConfig).to_string()
99 }
100}
101
102#[pymethods]
103#[pyo3_stub_gen::derive::gen_stub_pymethods]
104impl LighterExecutionClientConfig {
105 #[new]
107 #[pyo3(signature = (
108 account_id,
109 account_index = None,
110 api_key_index = None,
111 private_key = None,
112 base_url_http = None,
113 base_url_ws = None,
114 proxy_url = None,
115 environment = None,
116 http_timeout_secs = None,
117 ws_timeout_secs = None,
118 market_order_slippage_bps = None,
119 rest_quota_per_min = None,
120 sendtx_quota_per_min = None,
121 transport_backend = None,
122 deployment = None,
123 venue = None,
124 use_gtd = true,
125 scheduled_cancel_timeout_secs = None,
126 ))]
127 #[expect(clippy::too_many_arguments)]
128 fn py_new(
129 account_id: AccountId,
130 account_index: Option<u64>,
131 api_key_index: Option<u8>,
132 private_key: Option<String>,
133 base_url_http: Option<String>,
134 base_url_ws: Option<String>,
135 proxy_url: Option<String>,
136 environment: Option<LighterEnvironment>,
137 http_timeout_secs: Option<u64>,
138 ws_timeout_secs: Option<u64>,
139 market_order_slippage_bps: Option<u32>,
140 rest_quota_per_min: Option<u32>,
141 sendtx_quota_per_min: Option<u32>,
142 transport_backend: Option<TransportBackend>,
143 deployment: Option<LighterDeployment>,
144 venue: Option<Venue>,
145 use_gtd: bool,
146 scheduled_cancel_timeout_secs: Option<u64>,
147 ) -> PyResult<Self> {
148 validate_scheduled_cancel_timeout(scheduled_cancel_timeout_secs).map_err(to_pyvalue_err)?;
149 let defaults = Self::default();
150 Ok(Self {
151 environment: environment.unwrap_or(defaults.environment),
152 deployment: deployment.unwrap_or(defaults.deployment),
153 venue,
154 account_id,
155 account_index,
156 api_key_index,
157 private_key: private_key.map(SecretString::from),
158 base_url_http,
159 base_url_ws,
160 proxy_url: proxy_url.map(SecretString::from),
161 http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
162 ws_timeout_secs: ws_timeout_secs.unwrap_or(defaults.ws_timeout_secs),
163 market_order_slippage_bps: market_order_slippage_bps
164 .unwrap_or(defaults.market_order_slippage_bps),
165 rest_quota_per_min,
166 sendtx_quota_per_min,
167 transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
168 use_gtd,
169 scheduled_cancel_timeout_secs,
170 })
171 }
172
173 #[getter]
174 const fn has_proxy_url(&self) -> bool {
175 self.proxy_url.is_some()
176 }
177
178 fn __repr__(&self) -> String {
179 stringify!(LighterExecutionClientConfig).to_string()
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use pyo3::{
186 PyRef, Python,
187 types::{PyAnyMethods, PyDict, PyDictMethods},
188 };
189 use rstest::rstest;
190
191 use super::*;
192
193 #[rstest]
194 #[case(None, true)]
195 #[case(Some(420), true)]
196 #[case(Some(1_295_940), true)]
197 #[case(Some(419), false)]
198 #[case(Some(1_295_941), false)]
199 fn test_python_scheduled_cancel(#[case] timeout: Option<u64>, #[case] valid: bool) {
200 Python::initialize();
201 Python::attach(|py| {
202 let kwargs = PyDict::new(py);
203 kwargs
204 .set_item("account_id", AccountId::from("LIGHTER-123"))
205 .unwrap();
206
207 if let Some(timeout) = timeout {
208 kwargs
209 .set_item("scheduled_cancel_timeout_secs", timeout)
210 .unwrap();
211 }
212
213 let result = py
214 .get_type::<LighterExecutionClientConfig>()
215 .call((), Some(&kwargs));
216 assert_eq!(result.is_ok(), valid);
217
218 if valid {
219 let config = result.unwrap();
220 assert_eq!(
221 config
222 .getattr("scheduled_cancel_timeout_secs")
223 .unwrap()
224 .extract::<Option<u64>>()
225 .unwrap(),
226 timeout
227 );
228 assert_eq!(
229 config
230 .extract::<PyRef<LighterExecutionClientConfig>>()
231 .unwrap()
232 .scheduled_cancel_timeout_secs,
233 timeout
234 );
235 } else {
236 assert!(
237 result
238 .unwrap_err()
239 .is_instance_of::<pyo3::exceptions::PyValueError>(py)
240 );
241 }
242 });
243 }
244
245 #[rstest]
246 fn test_data_config_py_new_book_timeout() {
247 let config = LighterDataClientConfig::py_new(
248 None,
249 None,
250 None,
251 None,
252 None,
253 None,
254 None,
255 None,
256 None,
257 None,
258 Some(7),
259 None,
260 None,
261 None,
262 Some(42),
263 );
264
265 assert_eq!(config.rest_quota_per_min, Some(7));
266 assert_eq!(config.book_snapshot_timeout_secs, 42);
267 }
268
269 #[rstest]
270 fn test_data_config_py_new_book_timeout_default() {
271 let config = LighterDataClientConfig::py_new(
272 None, None, None, None, None, None, None, None, None, None, None, None, None, None,
273 None,
274 );
275
276 assert_eq!(config.book_snapshot_timeout_secs, 10);
277 }
278}