nautilus_persistence/python/backend/
session.rs1use std::collections::HashMap;
17
18use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyruntime_err};
19use nautilus_model::{
20 data::{
21 Bar, Data, DataFFI, InstrumentStatus, MarkPriceUpdate, OptionGreeks, OrderBookDelta,
22 OrderBookDepth10, QuoteTick, TradeTick,
23 },
24 python::data::{DATA_FFI_CVEC_CAPSULE_NAME, DataFfiCVec},
25};
26use nautilus_serialization::arrow::{ArrowSchemaProvider, custom::CustomDataDecoder};
27use pyo3::{prelude::*, types::PyCapsule};
28
29use crate::backend::session::{DataBackendSession, DataQueryResult};
30
31struct SendPtr<T>(*mut T);
33
34unsafe impl<T> Send for SendPtr<T> {}
36
37#[allow(
39 clippy::match_wildcard_for_single_variants,
40 reason = "Data::Defi appears through nautilus-model feature unification"
41)]
42fn data_to_pyobject(py: Python<'_>, item: Data) -> PyResult<Py<PyAny>> {
43 match item {
44 Data::Quote(quote) => Py::new(py, quote).map(pyo3::Py::into_any),
45 Data::Trade(trade) => Py::new(py, trade).map(pyo3::Py::into_any),
46 Data::Bar(bar) => Py::new(py, bar).map(pyo3::Py::into_any),
47 Data::Delta(delta) => Py::new(py, delta).map(pyo3::Py::into_any),
48 Data::Deltas(deltas) => Py::new(py, (*deltas).clone()).map(pyo3::Py::into_any),
49 Data::Depth10(depth) => Py::new(py, *depth).map(pyo3::Py::into_any),
50 Data::IndexPriceUpdate(price) => Py::new(py, price).map(pyo3::Py::into_any),
51 Data::MarkPriceUpdate(price) => Py::new(py, price).map(pyo3::Py::into_any),
52 Data::FundingRateUpdate(funding_rate) => Py::new(py, funding_rate).map(pyo3::Py::into_any),
53 Data::OptionGreeks(greeks) => Py::new(py, greeks).map(pyo3::Py::into_any),
54 Data::InstrumentStatus(status) => Py::new(py, status).map(pyo3::Py::into_any),
55 Data::InstrumentClose(close) => Py::new(py, close).map(pyo3::Py::into_any),
56 Data::Custom(custom) => Py::new(py, custom).map(pyo3::Py::into_any),
57 #[cfg(feature = "defi")]
58 Data::Defi(_) => Err(to_pyruntime_err("Unsupported Data::Defi variant")),
59 #[allow(unreachable_patterns)]
60 _ => Err(to_pyruntime_err("Unsupported Data variant")),
61 }
62}
63
64#[repr(C)]
65#[pyclass(frozen, eq, eq_int, from_py_object)]
66#[pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.persistence")]
67#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
68pub enum NautilusDataType {
69 OrderBookDelta = 1,
71 OrderBookDepth10 = 2,
72 QuoteTick = 3,
73 TradeTick = 4,
74 Bar = 5,
75 MarkPriceUpdate = 6,
76 OptionGreeks = 7,
77 InstrumentStatus = 8,
78}
79
80#[pymethods]
81#[pyo3_stub_gen::derive::gen_stub_pymethods]
82impl NautilusDataType {
83 #[expect(
84 clippy::trivially_copy_pass_by_ref,
85 reason = "PyO3 special methods use a borrowed receiver"
86 )]
87 const fn __hash__(&self) -> isize {
88 *self as isize
89 }
90}
91
92#[pymethods]
93#[pyo3_stub_gen::derive::gen_stub_pymethods]
94impl DataBackendSession {
95 #[new]
96 #[pyo3(signature=(chunk_size=10_000))]
97 fn new_session(chunk_size: usize) -> Self {
98 Self::new(chunk_size)
99 }
100
101 #[pyo3(name = "add_file")]
114 #[pyo3(signature = (data_type, table_name, file_path, sql_query=None))]
115 fn py_add_file(
116 mut slf: PyRefMut<'_, Self>,
117 data_type: NautilusDataType,
118 table_name: &str,
119 file_path: &str,
120 sql_query: Option<&str>,
121 ) -> PyResult<()> {
122 let _guard = slf.runtime.enter();
123
124 match data_type {
125 NautilusDataType::OrderBookDelta => slf
126 .add_file::<OrderBookDelta>(table_name, file_path, sql_query, None)
127 .map_err(to_pyruntime_err),
128 NautilusDataType::OrderBookDepth10 => slf
129 .add_file::<OrderBookDepth10>(table_name, file_path, sql_query, None)
130 .map_err(to_pyruntime_err),
131 NautilusDataType::QuoteTick => slf
132 .add_file::<QuoteTick>(table_name, file_path, sql_query, None)
133 .map_err(to_pyruntime_err),
134 NautilusDataType::TradeTick => slf
135 .add_file::<TradeTick>(table_name, file_path, sql_query, None)
136 .map_err(to_pyruntime_err),
137 NautilusDataType::Bar => slf
138 .add_file::<Bar>(table_name, file_path, sql_query, None)
139 .map_err(to_pyruntime_err),
140 NautilusDataType::MarkPriceUpdate => slf
141 .add_file::<MarkPriceUpdate>(table_name, file_path, sql_query, None)
142 .map_err(to_pyruntime_err),
143 NautilusDataType::OptionGreeks => slf
144 .add_file::<OptionGreeks>(table_name, file_path, sql_query, None)
145 .map_err(to_pyruntime_err),
146 NautilusDataType::InstrumentStatus => slf
147 .add_file::<InstrumentStatus>(table_name, file_path, sql_query, None)
148 .map_err(to_pyruntime_err),
149 }
150 }
151
152 #[pyo3(name = "add_custom_file")]
157 #[pyo3(signature = (type_name, table_name, file_path, sql_query=None))]
158 fn py_add_custom_file(
159 mut slf: PyRefMut<'_, Self>,
160 type_name: &str,
161 table_name: &str,
162 file_path: &str,
163 sql_query: Option<&str>,
164 ) -> PyResult<()> {
165 let _guard = slf.runtime.enter();
166 let mut metadata = HashMap::new();
167 metadata.insert("type_name".to_string(), type_name.to_string());
168 let base_schema = CustomDataDecoder::get_schema(Some(metadata));
169 base_schema.field_with_name("ts_init").map_err(|_| {
170 to_pyruntime_err(format!(
171 "custom data type '{type_name}' is not registered with an Arrow schema containing ts_init"
172 ))
173 })?;
174 slf.add_file::<CustomDataDecoder>(table_name, file_path, sql_query, Some(type_name))
178 .map_err(to_pyruntime_err)
179 }
180
181 fn to_query_result(mut slf: PyRefMut<'_, Self>) -> DataQueryResult {
182 let py = slf.py();
183 let chunk_size = slf.chunk_size;
184 let ptr = SendPtr(&raw mut *slf);
185
186 let query_result = unsafe {
191 py.detach(move || {
192 let p = ptr;
193 (*p.0).get_query_result()
194 })
195 };
196
197 DataQueryResult::new(query_result, chunk_size)
198 }
199
200 #[pyo3(name = "register_object_store_from_uri")]
202 #[pyo3(signature = (uri, storage_options=None))]
203 fn py_register_object_store_from_uri(
204 mut slf: PyRefMut<'_, Self>,
205 uri: &str,
206 storage_options: Option<HashMap<String, String>>,
207 ) -> PyResult<()> {
208 let storage_options = storage_options.map(|m| m.into_iter().collect());
210 slf.register_object_store_from_uri(uri, storage_options)
211 .map_err(to_pyruntime_err)
212 }
213}
214
215#[pymethods]
216#[pyo3_stub_gen::derive::gen_stub_pymethods]
217impl DataQueryResult {
218 const fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
220 slf
221 }
222
223 fn __next__(mut slf: PyRefMut<'_, Self>) -> PyResult<Option<Py<PyAny>>> {
229 let py = slf.py();
230 let ptr = SendPtr(&raw mut *slf);
231
232 let acc = unsafe {
242 py.detach(move || {
243 let p = ptr;
244 (*p.0).next()
245 })
246 };
247
248 match acc {
249 Some(acc) if !acc.is_empty() => {
250 let has_non_ffi = acc.iter().any(|d| {
251 matches!(
252 d,
253 Data::Custom(_)
254 | Data::FundingRateUpdate(_)
255 | Data::OptionGreeks(_)
256 | Data::InstrumentStatus(_)
257 )
258 });
259
260 if has_non_ffi {
261 let objects: Vec<Py<PyAny>> = acc
263 .into_iter()
264 .map(|item| data_to_pyobject(py, item))
265 .collect::<PyResult<_>>()?;
266 Ok(Some(objects.into_py_any_unwrap(py)))
267 } else {
268 let ffi_data: Vec<DataFFI> = acc
270 .into_iter()
271 .map(DataFFI::try_from)
272 .collect::<Result<Vec<_>, _>>()
273 .map_err(to_pyruntime_err)?;
274 let cvec: DataFfiCVec = ffi_data.into();
275 match PyCapsule::new_with_value_and_destructor::<DataFfiCVec, _>(
276 py,
277 cvec,
278 DATA_FFI_CVEC_CAPSULE_NAME,
279 |_, _| {},
280 ) {
281 Ok(capsule) => Ok(Some(capsule.into_py_any_unwrap(py))),
282 Err(e) => Err(to_pyruntime_err(e)),
283 }
284 }
285 }
286 _ => Ok(None),
287 }
288 }
289}