nautilus_infrastructure/python/redis/
cache.rs1use bytes::Bytes;
17use nautilus_common::{cache::CacheConfig, live::get_runtime};
18use nautilus_core::{
19 UUID4,
20 python::{to_pyruntime_err, to_pyvalue_err},
21};
22use nautilus_model::{
23 data::{CustomData, DataType},
24 identifiers::{AccountId, ClientOrderId, PositionId, TraderId},
25 python::{
26 account::account_any_to_pyobject, instruments::instrument_any_to_pyobject,
27 orders::order_any_to_pyobject,
28 },
29};
30use pyo3::{
31 IntoPyObjectExt,
32 prelude::*,
33 types::{PyBytes, PyDict},
34};
35use serde_json::Value;
36
37use crate::redis::{
38 cache::{RedisCacheConfig, RedisCacheDatabase},
39 queries::DatabaseQueries,
40};
41
42#[pymethods]
43impl RedisCacheDatabase {
44 #[new]
46 #[pyo3(signature = (trader_id, instance_id, config_json, database_config_json=None))]
47 fn py_new(
48 trader_id: TraderId,
49 instance_id: UUID4,
50 config_json: &[u8],
51 database_config_json: Option<&[u8]>,
52 ) -> PyResult<Self> {
53 let (config, database) = parse_inputs(config_json, database_config_json)?;
54 let result = get_runtime()
55 .block_on(async { Self::new(trader_id, instance_id, config, database).await });
56 result.map_err(to_pyruntime_err)
57 }
58
59 #[pyo3(name = "close")]
60 fn py_close(&mut self) {
61 self.close();
62 }
63
64 #[pyo3(name = "flushdb")]
65 fn py_flushdb(&mut self) {
66 get_runtime().block_on(async { self.flushdb().await });
67 }
68
69 #[pyo3(name = "keys")]
75 fn py_keys(&mut self, pattern: &str) -> PyResult<Vec<String>> {
76 let result = get_runtime().block_on(async { self.keys(pattern).await });
77 result.map_err(to_pyruntime_err)
78 }
79
80 #[pyo3(name = "load_all")]
81 fn py_load_all(&mut self) -> PyResult<Py<PyAny>> {
82 let result = get_runtime().block_on(async {
83 DatabaseQueries::load_all(&self.con, self.get_encoding(), self.get_trader_key()).await
84 });
85
86 match result {
87 Ok(cache_map) => Python::attach(|py| {
88 let dict = PyDict::new(py);
89
90 let currencies_dict = PyDict::new(py);
92 for (key, value) in cache_map.currencies {
93 currencies_dict
94 .set_item(key.to_string(), value)
95 .map_err(to_pyvalue_err)?;
96 }
97 dict.set_item("currencies", currencies_dict)
98 .map_err(to_pyvalue_err)?;
99
100 let instruments_dict = PyDict::new(py);
102 for (key, value) in cache_map.instruments {
103 let py_object = instrument_any_to_pyobject(py, value)?;
104 instruments_dict
105 .set_item(key, py_object)
106 .map_err(to_pyvalue_err)?;
107 }
108 dict.set_item("instruments", instruments_dict)
109 .map_err(to_pyvalue_err)?;
110
111 let synthetics_dict = PyDict::new(py);
113 for (key, value) in cache_map.synthetics {
114 synthetics_dict
115 .set_item(key, value)
116 .map_err(to_pyvalue_err)?;
117 }
118 dict.set_item("synthetics", synthetics_dict)
119 .map_err(to_pyvalue_err)?;
120
121 let accounts_dict = PyDict::new(py);
123 for (key, value) in cache_map.accounts {
124 let py_object = account_any_to_pyobject(py, value)?;
125 accounts_dict
126 .set_item(key, py_object)
127 .map_err(to_pyvalue_err)?;
128 }
129 dict.set_item("accounts", accounts_dict)
130 .map_err(to_pyvalue_err)?;
131
132 let orders_dict = PyDict::new(py);
134 for (key, value) in cache_map.orders {
135 let py_object = order_any_to_pyobject(py, value)?;
136 orders_dict
137 .set_item(key, py_object)
138 .map_err(to_pyvalue_err)?;
139 }
140 dict.set_item("orders", orders_dict)
141 .map_err(to_pyvalue_err)?;
142
143 let positions_dict = PyDict::new(py);
145 for (key, value) in cache_map.positions {
146 positions_dict
147 .set_item(key, value)
148 .map_err(to_pyvalue_err)?;
149 }
150 dict.set_item("positions", positions_dict)
151 .map_err(to_pyvalue_err)?;
152
153 dict.into_py_any(py)
154 }),
155 Err(e) => Err(to_pyruntime_err(e)),
156 }
157 }
158
159 #[pyo3(name = "read")]
165 fn py_read(&mut self, py: Python, key: &str) -> PyResult<Vec<Py<PyAny>>> {
166 let result = get_runtime().block_on(async { self.read(key).await });
167 match result {
168 Ok(result) => {
169 let vec_py_bytes = result
170 .into_iter()
171 .map(|r| PyBytes::new(py, r.as_ref()).into())
172 .collect::<Vec<Py<PyAny>>>();
173 Ok(vec_py_bytes)
174 }
175 Err(e) => Err(to_pyruntime_err(e)),
176 }
177 }
178
179 #[pyo3(name = "read_bulk")]
185 #[expect(clippy::needless_pass_by_value)]
186 fn py_read_bulk(&mut self, py: Python, keys: Vec<String>) -> PyResult<Vec<Option<Py<PyAny>>>> {
187 let result = get_runtime().block_on(async { self.read_bulk(&keys).await });
188 match result {
189 Ok(results) => {
190 let vec_py_bytes = results
191 .into_iter()
192 .map(|opt| opt.map(|bytes| PyBytes::new(py, bytes.as_ref()).into()))
193 .collect::<Vec<Option<Py<PyAny>>>>();
194 Ok(vec_py_bytes)
195 }
196 Err(e) => Err(to_pyruntime_err(e)),
197 }
198 }
199
200 #[pyo3(name = "insert")]
206 fn py_insert(&mut self, key: String, payload: Vec<Vec<u8>>) -> PyResult<()> {
207 let payload: Vec<Bytes> = payload.into_iter().map(Bytes::from).collect();
208 self.insert(key, Some(payload)).map_err(to_pyvalue_err)
209 }
210
211 #[pyo3(name = "update")]
217 fn py_update(&mut self, key: String, payload: Vec<Vec<u8>>) -> PyResult<()> {
218 let payload: Vec<Bytes> = payload.into_iter().map(Bytes::from).collect();
219 self.update(key, Some(payload)).map_err(to_pyvalue_err)
220 }
221
222 #[pyo3(name = "delete")]
228 #[pyo3(signature = (key, payload=None))]
229 fn py_delete(&mut self, key: String, payload: Option<Vec<Vec<u8>>>) -> PyResult<()> {
230 let payload: Option<Vec<Bytes>> =
231 payload.map(|vec| vec.into_iter().map(Bytes::from).collect());
232 self.delete(key, payload).map_err(to_pyvalue_err)
233 }
234
235 #[pyo3(name = "delete_order")]
241 fn py_delete_order(&mut self, client_order_id: &str) -> PyResult<()> {
242 let client_order_id = ClientOrderId::new(client_order_id);
243 self.delete_order(&client_order_id).map_err(to_pyvalue_err)
244 }
245
246 #[pyo3(name = "delete_position")]
252 fn py_delete_position(&mut self, position_id: &str) -> PyResult<()> {
253 let position_id = PositionId::new(position_id);
254 self.delete_position(&position_id).map_err(to_pyvalue_err)
255 }
256
257 #[pyo3(name = "delete_account_event")]
263 fn py_delete_account_event(&mut self, account_id: &str, event_id: &str) -> PyResult<()> {
264 let account_id = AccountId::new(account_id);
265 self.delete_account_event(&account_id, event_id)
266 .map_err(to_pyvalue_err)
267 }
268
269 #[pyo3(name = "add_custom_data")]
275 #[expect(clippy::needless_pass_by_value)]
276 fn py_add_custom_data(&mut self, data: CustomData) -> PyResult<()> {
277 self.add_custom_data(&data).map_err(to_pyvalue_err)
278 }
279
280 #[pyo3(name = "load_custom_data")]
286 #[expect(clippy::needless_pass_by_value)]
287 fn py_load_custom_data(
288 &mut self,
289 py: Python<'_>,
290 data_type: DataType,
291 ) -> PyResult<Vec<CustomData>> {
292 py.detach(|| self.load_custom_data(&data_type).map_err(to_pyvalue_err))
293 }
294}
295
296fn parse_inputs(
297 config_json: &[u8],
298 database_config_json: Option<&[u8]>,
299) -> PyResult<(CacheConfig, RedisCacheConfig)> {
300 let mut config_value: Value = serde_json::from_slice(config_json).map_err(to_pyvalue_err)?;
301 let legacy_database = config_value
303 .as_object_mut()
304 .and_then(|object| object.remove("database"));
305
306 let config = serde_json::from_value(config_value).map_err(to_pyvalue_err)?;
307 let database = match database_config_json {
308 Some(raw) => serde_json::from_slice(raw).map_err(to_pyvalue_err)?,
309 None => match legacy_database {
310 Some(value) => config_from_legacy_database(value)?,
311 None => RedisCacheConfig::default(),
312 },
313 };
314
315 Ok((config, database))
316}
317
318fn config_from_legacy_database(mut value: Value) -> PyResult<RedisCacheConfig> {
319 if value.is_null() {
320 return Ok(RedisCacheConfig::default());
321 }
322
323 remove_legacy_selector(&mut value, "cache database")?;
324 serde_json::from_value(value).map_err(to_pyvalue_err)
325}
326
327fn remove_legacy_selector(value: &mut Value, label: &str) -> PyResult<()> {
328 let Some(object) = value.as_object_mut() else {
329 return Ok(());
330 };
331
332 let selector = object
333 .remove("database_type")
334 .or_else(|| object.remove("type"));
335 let Some(selector) = selector else {
336 return Ok(());
337 };
338 let Some(selector) = selector.as_str() else {
339 return Err(to_pyvalue_err(format!(
340 "invalid {label} type selector, expected string"
341 )));
342 };
343
344 if selector != "redis" {
345 return Err(to_pyvalue_err(format!(
346 "invalid {label} type selector, expected 'redis', was '{selector}'"
347 )));
348 }
349
350 Ok(())
351}
352
353#[cfg(test)]
354mod tests {
355 use rstest::rstest;
356 use serde_json::json;
357
358 use super::*;
359
360 #[rstest]
361 fn test_parse_inputs_accepts_legacy_database() {
362 let config_json = serde_json::to_vec(&json!({
363 "database": {
364 "type": "redis",
365 "host": "redis.example.com",
366 "port": 6380,
367 "password": "secret",
368 "ssl": true,
369 },
370 "encoding": "json",
371 "buffer_interval_ms": 25,
372 }))
373 .unwrap();
374
375 let (config, database) = parse_inputs(&config_json, None).unwrap();
376
377 assert_eq!(config.buffer_interval_ms, Some(25));
378 assert_eq!(database.host, Some("redis.example.com".to_string()));
379 assert_eq!(database.port, Some(6380));
380 assert_eq!(database.password, Some("secret".to_string()));
381 assert!(database.ssl);
382 }
383
384 #[rstest]
385 fn test_parse_inputs_defaults_null_legacy_database() {
386 let config_json = serde_json::to_vec(&json!({
387 "database": null,
388 "buffer_interval_ms": 50,
389 }))
390 .unwrap();
391
392 let (config, database) = parse_inputs(&config_json, None).unwrap();
393
394 assert_eq!(config.buffer_interval_ms, Some(50));
395 assert_eq!(database, RedisCacheConfig::default());
396 }
397
398 #[rstest]
399 fn test_parse_inputs_prefers_explicit_database_config() {
400 let config_json = serde_json::to_vec(&json!({
401 "database": {
402 "type": "redis",
403 "host": "legacy.example.com",
404 },
405 }))
406 .unwrap();
407 let database_config_json = serde_json::to_vec(&json!({
408 "host": "explicit.example.com",
409 "port": 6381,
410 }))
411 .unwrap();
412
413 let (_, database) = parse_inputs(&config_json, Some(&database_config_json)).unwrap();
414
415 assert_eq!(database.host, Some("explicit.example.com".to_string()));
416 assert_eq!(database.port, Some(6381));
417 }
418
419 #[rstest]
420 fn test_parse_inputs_rejects_non_redis_legacy_database() {
421 Python::initialize();
422 let config_json = serde_json::to_vec(&json!({
423 "database": {
424 "type": "postgres",
425 },
426 }))
427 .unwrap();
428
429 let error = parse_inputs(&config_json, None).unwrap_err();
430
431 assert_eq!(
432 error.to_string(),
433 "ValueError: invalid cache database type selector, expected 'redis', was 'postgres'"
434 );
435 }
436}