1#![expect(
17 clippy::too_many_arguments,
18 reason = "PyO3 catalog wrapper mirrors Python API dispatch surface"
19)]
20
21use std::collections::HashMap;
22
23use nautilus_common::{
24 config::ConfigError, enums::Environment, python::config_error_to_pyvalue_err,
25};
26use nautilus_core::{
27 UnixNanos,
28 python::{to_pytype_err, to_pyvalue_err},
29};
30use nautilus_model::{
31 data::{
32 Bar, Data, FundingRateUpdate, HasTsInit, IndexPriceUpdate, InstrumentStatus,
33 MarkPriceUpdate, NautilusDataType, OptionGreeks, OrderBookDelta, OrderBookDepth, QuoteTick,
34 TradeTick, close::InstrumentClose,
35 },
36 python::{
37 data::data_to_pyobject,
38 instruments::{
39 PyNautilusInstrumentType, instrument_any_to_pyobject, pyobject_to_instrument_any,
40 },
41 },
42};
43use nautilus_serialization::{
44 arrow::{
45 DecodeTypedFromRecordBatch, EncodeToRecordBatch, display::instrument::encode_instruments,
46 },
47 python::arrow::{arrow_record_batches_to_pyarrow_stream, arrow_record_batches_to_pybytes},
48};
49use pyo3::{
50 exceptions::PyIOError,
51 prelude::*,
52 types::{PyBytes, PyDict, PyList},
53};
54
55use crate::{
56 backend::{migration::build_catalog_migration_plan, parquet::catalog::ParquetDataCatalog},
57 catalog::{
58 traits::{CatalogQuery, CatalogReader, CatalogRecordQuery, CatalogWriter},
59 types::{HasCatalogDataType, parquet_catalog_data_type_path_prefixes},
60 },
61 config::CatalogCompression,
62 python::backend::{
63 PyCatalogDataType, arrow_ipc_batches, arrow_ipc_data_schema, arrow_ipc_record_schema,
64 arrow_record_batches_from_pybytes, catalog_metadata_to_pydict, catalog_record_type_from_py,
65 nautilus_data_type_from_py, to_pyio_err, write_record_params_from_py,
66 },
67};
68
69#[expect(
70 clippy::needless_pass_by_value,
71 reason = "PyO3 supplies owned Python data at the catalog write boundary"
72)]
73fn write_parquet_data<T>(
74 catalog: &ParquetDataCatalog,
75 data: Vec<T>,
76 start: Option<u64>,
77 end: Option<u64>,
78 skip_disjoint_check: bool,
79 label: &str,
80) -> PyResult<String>
81where
82 T: HasTsInit + EncodeToRecordBatch + HasCatalogDataType,
83{
84 catalog
85 .write_to_parquet(
86 &data,
87 start.map(UnixNanos::from),
88 end.map(UnixNanos::from),
89 Some(skip_disjoint_check),
90 )
91 .map(|path| path.to_string_lossy().to_string())
92 .map_err(|e| PyIOError::new_err(format!("Failed to write {label}: {e}")))
93}
94
95fn query_parquet_data<T>(
96 catalog: &mut ParquetDataCatalog,
97 identifiers: Option<Vec<String>>,
98 start: Option<u64>,
99 end: Option<u64>,
100 where_clause: Option<&str>,
101 files: Option<Vec<String>>,
102 optimize_file_loading: bool,
103 error_context: &str,
104) -> PyResult<Vec<T>>
105where
106 T: DecodeTypedFromRecordBatch + HasCatalogDataType + HasTsInit,
107{
108 catalog
109 .query::<T>(
110 identifiers,
111 start.map(UnixNanos::from),
112 end.map(UnixNanos::from),
113 where_clause,
114 files,
115 optimize_file_loading,
116 )
117 .map_err(|e| PyIOError::new_err(format!("{error_context}: {e}")))
118}
119
120fn reject_parquet_as_of(as_of: Option<&Bound<'_, PyAny>>) -> PyResult<()> {
121 if as_of.is_some() {
122 return Err(to_pyvalue_err("ParquetDataCatalog does not support as_of"));
123 }
124
125 Ok(())
126}
127
128#[pyclass(name = "ParquetDataCatalog", module = "nautilus_trader.persistence")]
130#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.persistence")]
131pub struct PyParquetDataCatalog {
132 inner: ParquetDataCatalog,
133}
134
135#[pymethods]
136#[pyo3_stub_gen::derive::gen_stub_pymethods]
137impl PyParquetDataCatalog {
138 #[new]
153 #[pyo3(signature = (base_path, storage_options=None, batch_size=None, compression=None, max_row_group_size=None))]
154 pub fn py_new(
155 base_path: &str,
156 storage_options: Option<HashMap<String, String>>,
157 batch_size: Option<usize>,
158 compression: Option<u8>,
159 max_row_group_size: Option<usize>,
160 ) -> PyResult<Self> {
161 let compression = compression
162 .map(catalog_compression_from_code)
163 .transpose()?
164 .map(parquet::basic::Compression::from);
165
166 let storage_options = storage_options.map(|m| m.into_iter().collect());
168
169 let inner = ParquetDataCatalog::from_uri(
170 base_path,
171 storage_options,
172 batch_size,
173 compression,
174 max_row_group_size,
175 )
176 .map_err(|e| match e.downcast::<ConfigError>() {
177 Ok(e) => config_error_to_pyvalue_err(e),
178 Err(e) => PyIOError::new_err(format!("Failed to create ParquetDataCatalog: {e}")),
179 })?;
180
181 Ok(Self { inner })
182 }
183
184 #[pyo3(signature = (parquet_path, storage_options=None, dry_run=false))]
186 pub fn migrate_from_legacy_parquet_path(
187 mut slf: PyRefMut<'_, Self>,
188 parquet_path: &str,
189 storage_options: Option<HashMap<String, String>>,
190 dry_run: bool,
191 ) -> PyResult<usize> {
192 let storage_options = storage_options.map(|m| m.into_iter().collect());
193 let source = ParquetDataCatalog::from_uri(parquet_path, storage_options, None, None, None)
194 .map_err(to_pyio_err)?;
195 let py = slf.py();
196 if dry_run {
197 return py
198 .detach(|| {
199 let plan = build_catalog_migration_plan(&source)?;
200 plan.ensure_ready()?;
201 Ok::<usize, anyhow::Error>(0)
202 })
203 .map_err(to_pyio_err);
204 }
205
206 let inner = &mut slf.inner;
207 py.detach(|| {
208 inner
209 .migrate_from_legacy_parquet_catalog(&source)
210 .map(|report| report.migrated_rows)
211 })
212 .map_err(to_pyio_err)
213 }
214
215 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
227 pub fn write_quote_ticks(
228 &self,
229 data: Vec<QuoteTick>,
230 start: Option<u64>,
231 end: Option<u64>,
232 skip_disjoint_check: bool,
233 ) -> PyResult<String> {
234 write_parquet_data(
235 &self.inner,
236 data,
237 start,
238 end,
239 skip_disjoint_check,
240 "quote ticks",
241 )
242 }
243
244 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
256 pub fn write_trade_ticks(
257 &self,
258 data: Vec<TradeTick>,
259 start: Option<u64>,
260 end: Option<u64>,
261 skip_disjoint_check: bool,
262 ) -> PyResult<String> {
263 write_parquet_data(
264 &self.inner,
265 data,
266 start,
267 end,
268 skip_disjoint_check,
269 "trade ticks",
270 )
271 }
272
273 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
285 pub fn write_order_book_deltas(
286 &self,
287 data: Vec<OrderBookDelta>,
288 start: Option<u64>,
289 end: Option<u64>,
290 skip_disjoint_check: bool,
291 ) -> PyResult<String> {
292 write_parquet_data(
293 &self.inner,
294 data,
295 start,
296 end,
297 skip_disjoint_check,
298 "order book deltas",
299 )
300 }
301
302 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
314 pub fn write_bars(
315 &self,
316 data: Vec<Bar>,
317 start: Option<u64>,
318 end: Option<u64>,
319 skip_disjoint_check: bool,
320 ) -> PyResult<String> {
321 write_parquet_data(&self.inner, data, start, end, skip_disjoint_check, "bars")
322 }
323
324 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
336 pub fn write_order_book_depths(
337 &self,
338 data: Vec<OrderBookDepth>,
339 start: Option<u64>,
340 end: Option<u64>,
341 skip_disjoint_check: bool,
342 ) -> PyResult<String> {
343 write_parquet_data(
344 &self.inner,
345 data,
346 start,
347 end,
348 skip_disjoint_check,
349 "order book depths",
350 )
351 }
352
353 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
365 pub fn write_mark_price_updates(
366 &self,
367 data: Vec<MarkPriceUpdate>,
368 start: Option<u64>,
369 end: Option<u64>,
370 skip_disjoint_check: bool,
371 ) -> PyResult<String> {
372 write_parquet_data(
373 &self.inner,
374 data,
375 start,
376 end,
377 skip_disjoint_check,
378 "mark price updates",
379 )
380 }
381
382 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
394 pub fn write_index_price_updates(
395 &self,
396 data: Vec<IndexPriceUpdate>,
397 start: Option<u64>,
398 end: Option<u64>,
399 skip_disjoint_check: bool,
400 ) -> PyResult<String> {
401 write_parquet_data(
402 &self.inner,
403 data,
404 start,
405 end,
406 skip_disjoint_check,
407 "index price updates",
408 )
409 }
410
411 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
423 pub fn write_option_greeks(
424 &self,
425 data: Vec<OptionGreeks>,
426 start: Option<u64>,
427 end: Option<u64>,
428 skip_disjoint_check: bool,
429 ) -> PyResult<String> {
430 write_parquet_data(
431 &self.inner,
432 data,
433 start,
434 end,
435 skip_disjoint_check,
436 "option greeks",
437 )
438 }
439
440 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
442 pub fn write_instrument_statuses(
443 &self,
444 data: Vec<InstrumentStatus>,
445 start: Option<u64>,
446 end: Option<u64>,
447 skip_disjoint_check: bool,
448 ) -> PyResult<String> {
449 write_parquet_data(
450 &self.inner,
451 data,
452 start,
453 end,
454 skip_disjoint_check,
455 "instrument statuses",
456 )
457 }
458
459 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
461 pub fn write_instrument_closes(
462 &self,
463 data: Vec<InstrumentClose>,
464 start: Option<u64>,
465 end: Option<u64>,
466 skip_disjoint_check: bool,
467 ) -> PyResult<String> {
468 write_parquet_data(
469 &self.inner,
470 data,
471 start,
472 end,
473 skip_disjoint_check,
474 "instrument closes",
475 )
476 }
477
478 pub fn write_instruments(&self, instruments: &Bound<'_, PyAny>) -> PyResult<Vec<String>> {
492 let data = instruments;
493 let py = data.py();
494 let list = data.cast::<PyList>()?;
495 let mut instruments = Vec::with_capacity(list.len());
496 for item in list.iter() {
497 let py_item: Py<PyAny> = item.unbind();
498 let instrument = pyobject_to_instrument_any(py, py_item)?;
499 instruments.push(instrument);
500 }
501
502 self.inner
503 .write_instruments(instruments)
504 .map(|paths| {
505 paths
506 .into_iter()
507 .map(|p| p.to_string_lossy().to_string())
508 .collect()
509 })
510 .map_err(|e| PyIOError::new_err(format!("Failed to write instruments: {e}")))
511 }
512
513 #[pyo3(signature = (
526 instrument_ids=None,
527 start=None,
528 end=None,
529 where_clause=None,
530 instrument_type=None,
531 ))]
532 #[expect(clippy::needless_pass_by_value)]
533 pub fn instruments(
534 &mut self,
535 instrument_ids: Option<Vec<String>>,
536 start: Option<u64>,
537 end: Option<u64>,
538 where_clause: Option<&str>,
539 instrument_type: Option<PyRef<'_, PyNautilusInstrumentType>>,
540 ) -> PyResult<Vec<Py<PyAny>>> {
541 let instrument_type = instrument_type.map(|instrument_type| instrument_type.inner());
542 let rust_instruments = self
543 .inner
544 .query_instruments_filtered_with_where_and_type(
545 instrument_ids.as_deref(),
546 start.map(UnixNanos::from),
547 end.map(UnixNanos::from),
548 where_clause,
549 instrument_type.as_ref(),
550 )
551 .map_err(|e| PyIOError::new_err(format!("Failed to query instruments: {e}")))?;
552 Python::attach(|py| {
553 rust_instruments
554 .into_iter()
555 .map(|inst| instrument_any_to_pyobject(py, inst))
556 .collect()
557 })
558 }
559
560 #[pyo3(signature = (
562 instrument_ids=None,
563 start=None,
564 end=None,
565 where_clause=None,
566 instrument_type=None,
567 ))]
568 #[expect(clippy::needless_pass_by_value)]
569 pub fn query_instrument_arrow_bytes(
570 &mut self,
571 py: Python<'_>,
572 instrument_ids: Option<Vec<String>>,
573 start: Option<u64>,
574 end: Option<u64>,
575 where_clause: Option<&str>,
576 instrument_type: Option<PyRef<'_, PyNautilusInstrumentType>>,
577 ) -> PyResult<Py<PyBytes>> {
578 let instrument_type = instrument_type.map(|instrument_type| instrument_type.inner());
579
580 let instruments = py
581 .detach(|| {
582 self.inner.query_instruments_filtered_with_where_and_type(
583 instrument_ids.as_deref(),
584 start.map(UnixNanos::from),
585 end.map(UnixNanos::from),
586 where_clause,
587 instrument_type.as_ref(),
588 )
589 })
590 .map_err(|e| PyIOError::new_err(format!("Failed query instruments: {e}")))?;
591
592 let batch = encode_instruments(&instruments)
593 .map_err(|e| PyIOError::new_err(format!("Failed encode instruments: {e}")))?;
594 let schema = batch.schema().as_ref().clone();
595 arrow_record_batches_to_pybytes(py, &schema, &[batch])
596 }
597
598 #[pyo3(signature = (
600 instrument_ids=None,
601 start=None,
602 end=None,
603 where_clause=None,
604 instrument_type=None,
605 ))]
606 #[expect(clippy::needless_pass_by_value)]
607 pub fn query_instrument_arrow_stream(
608 &mut self,
609 py: Python<'_>,
610 instrument_ids: Option<Vec<String>>,
611 start: Option<u64>,
612 end: Option<u64>,
613 where_clause: Option<&str>,
614 instrument_type: Option<PyRef<'_, PyNautilusInstrumentType>>,
615 ) -> PyResult<Py<PyAny>> {
616 let instrument_type = instrument_type.map(|instrument_type| instrument_type.inner());
617
618 let instruments = py
619 .detach(|| {
620 self.inner.query_instruments_filtered_with_where_and_type(
621 instrument_ids.as_deref(),
622 start.map(UnixNanos::from),
623 end.map(UnixNanos::from),
624 where_clause,
625 instrument_type.as_ref(),
626 )
627 })
628 .map_err(|e| PyIOError::new_err(format!("Failed query instruments: {e}")))?;
629
630 let batch = encode_instruments(&instruments)
631 .map_err(|e| PyIOError::new_err(format!("Failed encode instruments: {e}")))?;
632 let schema = batch.schema().as_ref().clone();
633 arrow_record_batches_to_pyarrow_stream(py, &schema, vec![batch])
634 }
635
636 #[pyo3(signature = (data_type, identifier=None, *, start, end))]
645 #[expect(clippy::needless_pass_by_value)]
646 pub fn extend_file_name(
647 &self,
648 data_type: PyCatalogDataType,
649 identifier: Option<String>,
650 start: u64,
651 end: u64,
652 ) -> PyResult<()> {
653 let data_type = data_type.into_inner();
654 let start_nanos = UnixNanos::from(start);
655 let end_nanos = UnixNanos::from(end);
656
657 self.inner
658 .extend_file_name(&data_type, identifier.as_deref(), start_nanos, end_nanos)
659 .map_err(|e| PyIOError::new_err(format!("Failed to extend file name: {e}")))
660 }
661
662 #[pyo3(signature = (start=None, end=None, ensure_contiguous_files=None, deduplicate=None))]
671 pub fn consolidate_catalog(
672 &self,
673 start: Option<u64>,
674 end: Option<u64>,
675 ensure_contiguous_files: Option<bool>,
676 deduplicate: Option<bool>,
677 ) -> PyResult<()> {
678 let start_nanos = start.map(UnixNanos::from);
679 let end_nanos = end.map(UnixNanos::from);
680
681 self.inner
682 .consolidate_catalog(start_nanos, end_nanos, ensure_contiguous_files, deduplicate)
683 .map_err(|e| PyIOError::new_err(format!("Failed to consolidate catalog: {e}")))
684 }
685
686 #[pyo3(signature = (data_type, identifier=None, start=None, end=None, ensure_contiguous_files=None, deduplicate=None))]
697 #[expect(clippy::needless_pass_by_value)]
698 pub fn consolidate_data(
699 &mut self,
700 data_type: PyCatalogDataType,
701 identifier: Option<String>,
702 start: Option<u64>,
703 end: Option<u64>,
704 ensure_contiguous_files: Option<bool>,
705 deduplicate: Option<bool>,
706 ) -> PyResult<()> {
707 let data_type = data_type.into_inner();
708 let start_nanos = start.map(UnixNanos::from);
709 let end_nanos = end.map(UnixNanos::from);
710
711 self.inner
712 .consolidate_data(
713 &data_type,
714 identifier.as_deref(),
715 start_nanos,
716 end_nanos,
717 ensure_contiguous_files,
718 deduplicate,
719 )
720 .map_err(|e| PyIOError::new_err(format!("Failed to consolidate data: {e}")))
721 }
722
723 #[pyo3(signature = (period_nanos=None, start=None, end=None, ensure_contiguous_files=None))]
738 pub fn consolidate_catalog_by_period(
739 &mut self,
740 period_nanos: Option<u64>,
741 start: Option<u64>,
742 end: Option<u64>,
743 ensure_contiguous_files: Option<bool>,
744 ) -> PyResult<()> {
745 let start_nanos = start.map(UnixNanos::from);
746 let end_nanos = end.map(UnixNanos::from);
747
748 self.inner
749 .consolidate_catalog_by_period(
750 period_nanos,
751 start_nanos,
752 end_nanos,
753 ensure_contiguous_files,
754 )
755 .map_err(|e| {
756 PyIOError::new_err(format!("Failed to consolidate catalog by period: {e}"))
757 })
758 }
759
760 #[pyo3(signature = (data_type, identifier=None, period_nanos=None, start=None, end=None, ensure_contiguous_files=None))]
776 #[expect(clippy::needless_pass_by_value)]
777 pub fn consolidate_data_by_period(
778 &mut self,
779 data_type: PyCatalogDataType,
780 identifier: Option<String>,
781 period_nanos: Option<u64>,
782 start: Option<u64>,
783 end: Option<u64>,
784 ensure_contiguous_files: Option<bool>,
785 ) -> PyResult<()> {
786 let data_type = data_type.into_inner();
787 let start_nanos = start.map(UnixNanos::from);
788 let end_nanos = end.map(UnixNanos::from);
789
790 self.inner
791 .consolidate_data_by_period(
792 &data_type,
793 identifier.as_deref(),
794 period_nanos,
795 start_nanos,
796 end_nanos,
797 ensure_contiguous_files,
798 )
799 .map_err(|e| PyIOError::new_err(format!("Failed to consolidate data by period: {e}")))
800 }
801
802 pub fn reset_all_file_names(&self) -> PyResult<()> {
804 self.inner
805 .reset_all_file_names()
806 .map_err(|e| PyIOError::new_err(format!("Failed to reset catalog file names: {e}")))
807 }
808
809 #[pyo3(signature = (data_type, identifier=None))]
816 #[expect(clippy::needless_pass_by_value)]
817 pub fn reset_data_file_names(
818 &self,
819 data_type: PyCatalogDataType,
820 identifier: Option<String>,
821 ) -> PyResult<()> {
822 let data_type = data_type.into_inner();
823 self.inner
824 .reset_data_file_names(&data_type, identifier.as_deref())
825 .map_err(|e| PyIOError::new_err(format!("Failed to reset data file names: {e}")))
826 }
827
828 #[pyo3(signature = (start=None, end=None))]
850 pub fn delete_catalog_range(&mut self, start: Option<u64>, end: Option<u64>) -> PyResult<()> {
851 let start_nanos = start.map(UnixNanos::from);
852 let end_nanos = end.map(UnixNanos::from);
853
854 self.inner
855 .delete_catalog_range(start_nanos, end_nanos)
856 .map_err(|e| PyIOError::new_err(format!("Failed to delete catalog range: {e}")))
857 }
858
859 #[pyo3(signature = (data_type, identifier=None, start=None, end=None))]
881 #[expect(clippy::needless_pass_by_value)]
882 pub fn delete_data_range(
883 &mut self,
884 #[gen_stub(override_type(type_repr = "model.NautilusDataType"))] data_type: &Bound<
885 '_,
886 PyAny,
887 >,
888 identifier: Option<String>,
889 start: Option<u64>,
890 end: Option<u64>,
891 ) -> PyResult<()> {
892 let data_type = nautilus_data_type_from_py(data_type)?;
893 let start_nanos = start.map(UnixNanos::from);
894 let end_nanos = end.map(UnixNanos::from);
895
896 self.inner
897 .delete_data_range(&data_type, identifier.as_deref(), start_nanos, end_nanos)
898 .map_err(|e| PyIOError::new_err(format!("Failed to delete data range: {e}")))
899 }
900
901 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
909 pub fn write_custom_data(
910 &self,
911 _py: Python<'_>,
912 data: Vec<Bound<'_, PyAny>>,
913 start: Option<u64>,
914 end: Option<u64>,
915 skip_disjoint_check: bool,
916 ) -> PyResult<String> {
917 use nautilus_model::data::CustomData;
918
919 let mut custom_items: Vec<CustomData> = Vec::with_capacity(data.len());
920 for obj in data {
921 let custom = obj.extract::<CustomData>().map_err(|_| {
922 to_pytype_err(
923 "write_custom_data requires CustomData wrappers; wrap with CustomData(data_type=DataType(cls, metadata=...), data=...)",
924 )
925 })?;
926
927 custom_items.push(custom);
928 }
929
930 let start_nanos = start.map(UnixNanos::from);
931 let end_nanos = end.map(UnixNanos::from);
932
933 self.inner
934 .write_custom_data_batch(
935 &custom_items,
936 start_nanos,
937 end_nanos,
938 Some(skip_disjoint_check),
939 )
940 .map(|path| path.to_string_lossy().to_string())
941 .map_err(|e| PyIOError::new_err(format!("Failed to write custom data: {e}")))
942 }
943
944 pub fn list_instruments(&self, data_type: PyCatalogDataType) -> PyResult<Vec<String>> {
946 let data_type = data_type.into_inner();
947 self.inner
948 .list_instruments(&data_type)
949 .map_err(|e| PyIOError::new_err(format!("Failed to list instruments: {e}")))
950 }
951
952 pub fn list_parquet_files(
954 &self,
955 data_type: PyCatalogDataType,
956 identifier: &str,
957 ) -> PyResult<Vec<String>> {
958 let data_type = data_type.into_inner();
959 let list_files = |prefix: &str| -> PyResult<Vec<String>> {
960 let directory = self
961 .inner
962 .make_path(prefix, Some(identifier))
963 .map_err(|e| PyIOError::new_err(format!("Failed to list parquet files: {e}")))?;
964 self.inner
965 .list_parquet_files(&directory)
966 .map_err(|e| PyIOError::new_err(format!("Failed to list parquet files: {e}")))
967 };
968 let mut files = Vec::new();
969
970 for prefix in parquet_catalog_data_type_path_prefixes(&data_type) {
971 files.extend(list_files(prefix.as_ref())?);
972 }
973
974 Ok(files)
975 }
976
977 #[pyo3(signature = (data_type, identifiers=None, start=None, end=None))]
992 pub fn query_files(
993 &self,
994 data_type: PyCatalogDataType,
995 identifiers: Option<Vec<String>>,
996 start: Option<u64>,
997 end: Option<u64>,
998 ) -> PyResult<Vec<String>> {
999 let data_type = data_type.into_inner();
1000 let start_nanos = start.map(UnixNanos::from);
1001 let end_nanos = end.map(UnixNanos::from);
1002
1003 self.inner
1004 .query_files(&data_type, identifiers, start_nanos, end_nanos)
1005 .map_err(|e| PyIOError::new_err(format!("Failed to query files list: {e}")))
1006 }
1007
1008 #[pyo3(signature = (data_type, identifiers=None, start=None, end=None, where_clause=None))]
1010 pub fn query_metadata(
1011 &mut self,
1012 py: Python<'_>,
1013 #[gen_stub(override_type(type_repr = "model.NautilusDataType"))] data_type: &Bound<
1014 '_,
1015 PyAny,
1016 >,
1017 identifiers: Option<Vec<String>>,
1018 start: Option<u64>,
1019 end: Option<u64>,
1020 where_clause: Option<&str>,
1021 ) -> PyResult<Py<PyDict>> {
1022 let data_type = nautilus_data_type_from_py(data_type)?;
1023
1024 let metadata = py
1025 .detach(|| {
1026 CatalogReader::query_metadata(
1027 &mut self.inner,
1028 &CatalogQuery::new(data_type)
1029 .with_identifiers(identifiers)
1030 .with_range(start.map(UnixNanos::from), end.map(UnixNanos::from))
1031 .with_where_clause(where_clause.map(str::to_string)),
1032 )
1033 })
1034 .map_err(|e| PyIOError::new_err(format!("Metadata query failed: {e}")))?;
1035
1036 catalog_metadata_to_pydict(py, metadata)
1037 }
1038
1039 #[pyo3(signature = (
1041 data_type,
1042 identifiers=None,
1043 start=None,
1044 end=None,
1045 where_clause=None,
1046 display=true,
1047 as_of=None,
1048 ))]
1049 pub fn query_data_arrow_bytes(
1050 &mut self,
1051 py: Python<'_>,
1052 #[gen_stub(override_type(type_repr = "model.NautilusDataType"))] data_type: &Bound<
1053 '_,
1054 PyAny,
1055 >,
1056 identifiers: Option<Vec<String>>,
1057 start: Option<u64>,
1058 end: Option<u64>,
1059 where_clause: Option<&str>,
1060 display: bool,
1061 as_of: Option<&Bound<'_, PyAny>>,
1062 ) -> PyResult<Py<PyBytes>> {
1063 let data_type = nautilus_data_type_from_py(data_type)?;
1064 reject_parquet_as_of(as_of)?;
1065 let query = CatalogQuery::new(data_type.clone())
1066 .with_identifiers(identifiers)
1067 .with_range(start.map(UnixNanos::from), end.map(UnixNanos::from))
1068 .with_where_clause(where_clause.map(str::to_string));
1069
1070 let batches = py
1071 .detach(|| {
1072 if display {
1073 CatalogReader::query_display_record_batches(&mut self.inner, &query)
1074 } else {
1075 let data = CatalogReader::query_batch(&mut self.inner, &query)?
1076 .to_data_vec_for_compat();
1077 crate::common::arrow::data_to_arrow_batches(&data_type, data)
1078 }
1079 })
1080 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
1081
1082 let schema = arrow_ipc_data_schema(&data_type, &batches, display)?;
1083 let batches = arrow_ipc_batches(&schema, batches)?;
1084 arrow_record_batches_to_pybytes(py, &schema, &batches)
1085 }
1086
1087 #[pyo3(signature = (
1089 data_type,
1090 identifiers=None,
1091 start=None,
1092 end=None,
1093 where_clause=None,
1094 display=true,
1095 as_of=None,
1096 ))]
1097 pub fn query_data_arrow_stream(
1098 &mut self,
1099 py: Python<'_>,
1100 #[gen_stub(override_type(type_repr = "model.NautilusDataType"))] data_type: &Bound<
1101 '_,
1102 PyAny,
1103 >,
1104 identifiers: Option<Vec<String>>,
1105 start: Option<u64>,
1106 end: Option<u64>,
1107 where_clause: Option<&str>,
1108 display: bool,
1109 as_of: Option<&Bound<'_, PyAny>>,
1110 ) -> PyResult<Py<PyAny>> {
1111 let data_type = nautilus_data_type_from_py(data_type)?;
1112 reject_parquet_as_of(as_of)?;
1113 let query = CatalogQuery::new(data_type.clone())
1114 .with_identifiers(identifiers)
1115 .with_range(start.map(UnixNanos::from), end.map(UnixNanos::from))
1116 .with_where_clause(where_clause.map(str::to_string));
1117
1118 let batches = py
1119 .detach(|| {
1120 if display {
1121 CatalogReader::query_display_record_batches(&mut self.inner, &query)
1122 } else {
1123 let data = CatalogReader::query_batch(&mut self.inner, &query)?
1124 .to_data_vec_for_compat();
1125 crate::common::arrow::data_to_arrow_batches(&data_type, data)
1126 }
1127 })
1128 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
1129
1130 let schema = arrow_ipc_data_schema(&data_type, &batches, display)?;
1131 let batches = arrow_ipc_batches(&schema, batches)?;
1132 arrow_record_batches_to_pyarrow_stream(py, &schema, batches)
1133 }
1134
1135 #[pyo3(signature = (record_type, data, identifier=None, params=None))]
1137 pub fn write_record_arrow_bytes(
1138 &mut self,
1139 py: Python<'_>,
1140 #[gen_stub(override_type(type_repr = "model.NautilusRecordType"))] record_type: &Bound<
1141 '_,
1142 PyAny,
1143 >,
1144 data: Vec<u8>,
1145 identifier: Option<String>,
1146 params: Option<Py<PyDict>>,
1147 ) -> PyResult<()> {
1148 let record_type = catalog_record_type_from_py(record_type)?;
1149 let batches = arrow_record_batches_from_pybytes(data)?;
1150 let params = write_record_params_from_py(py, identifier, params)?;
1151
1152 py.detach(|| CatalogWriter::write_records(&mut self.inner, record_type, &batches, params))
1153 .map_err(|e| PyIOError::new_err(format!("Failed write records: {e}")))
1154 }
1155
1156 #[pyo3(signature = (
1158 record_type,
1159 identifier=None,
1160 start=None,
1161 end=None,
1162 where_clause=None,
1163 display=true,
1164 as_of=None,
1165 ))]
1166 pub fn query_record_arrow_bytes(
1167 &mut self,
1168 py: Python<'_>,
1169 #[gen_stub(override_type(type_repr = "model.NautilusRecordType"))] record_type: &Bound<
1170 '_,
1171 PyAny,
1172 >,
1173 identifier: Option<String>,
1174 start: Option<u64>,
1175 end: Option<u64>,
1176 where_clause: Option<&str>,
1177 display: bool,
1178 as_of: Option<&Bound<'_, PyAny>>,
1179 ) -> PyResult<Py<PyBytes>> {
1180 let record_type = catalog_record_type_from_py(record_type)?;
1181 reject_parquet_as_of(as_of)?;
1182 let start_nanos = start.map(UnixNanos::from);
1183 let end_nanos = end.map(UnixNanos::from);
1184
1185 let batches = py
1186 .detach(|| {
1187 if display {
1188 CatalogReader::query_record_display_batches(
1189 &mut self.inner,
1190 &CatalogRecordQuery::new(record_type)
1191 .with_identifier(identifier)
1192 .with_range(start_nanos, end_nanos)
1193 .with_where_clause(where_clause.map(str::to_string)),
1194 )
1195 } else {
1196 CatalogReader::query_record_batches(
1197 &mut self.inner,
1198 &CatalogRecordQuery::new(record_type)
1199 .with_identifier(identifier)
1200 .with_range(start_nanos, end_nanos)
1201 .with_where_clause(where_clause.map(str::to_string)),
1202 )
1203 }
1204 })
1205 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
1206
1207 let schema = arrow_ipc_record_schema(record_type, &batches)?;
1208 let batches = arrow_ipc_batches(&schema, batches)?;
1209
1210 arrow_record_batches_to_pybytes(py, &schema, &batches)
1211 }
1212
1213 #[pyo3(signature = (
1215 record_type,
1216 identifier=None,
1217 start=None,
1218 end=None,
1219 where_clause=None,
1220 display=true,
1221 as_of=None,
1222 ))]
1223 pub fn query_record_arrow_stream(
1224 &mut self,
1225 py: Python<'_>,
1226 #[gen_stub(override_type(type_repr = "model.NautilusRecordType"))] record_type: &Bound<
1227 '_,
1228 PyAny,
1229 >,
1230 identifier: Option<String>,
1231 start: Option<u64>,
1232 end: Option<u64>,
1233 where_clause: Option<&str>,
1234 display: bool,
1235 as_of: Option<&Bound<'_, PyAny>>,
1236 ) -> PyResult<Py<PyAny>> {
1237 let record_type = catalog_record_type_from_py(record_type)?;
1238 reject_parquet_as_of(as_of)?;
1239 let start_nanos = start.map(UnixNanos::from);
1240 let end_nanos = end.map(UnixNanos::from);
1241
1242 let batches = py
1243 .detach(|| {
1244 if display {
1245 CatalogReader::query_record_display_batches(
1246 &mut self.inner,
1247 &CatalogRecordQuery::new(record_type)
1248 .with_identifier(identifier)
1249 .with_range(start_nanos, end_nanos)
1250 .with_where_clause(where_clause.map(str::to_string)),
1251 )
1252 } else {
1253 CatalogReader::query_record_batches(
1254 &mut self.inner,
1255 &CatalogRecordQuery::new(record_type)
1256 .with_identifier(identifier)
1257 .with_range(start_nanos, end_nanos)
1258 .with_where_clause(where_clause.map(str::to_string)),
1259 )
1260 }
1261 })
1262 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
1263
1264 let schema = arrow_ipc_record_schema(record_type, &batches)?;
1265 let batches = arrow_ipc_batches(&schema, batches)?;
1266
1267 arrow_record_batches_to_pyarrow_stream(py, &schema, batches)
1268 }
1269
1270 #[pyo3(signature = (start, end, data_type, identifier=None))]
1283 #[expect(clippy::needless_pass_by_value)]
1284 pub fn get_missing_intervals_for_request(
1285 &self,
1286 start: u64,
1287 end: u64,
1288 data_type: PyCatalogDataType,
1289 identifier: Option<String>,
1290 ) -> PyResult<Vec<(u64, u64)>> {
1291 let data_type = data_type.into_inner();
1292 self.inner
1293 .get_missing_intervals_for_request(start, end, &data_type, identifier.as_deref())
1294 .map_err(|e| PyIOError::new_err(format!("Failed to get missing intervals: {e}")))
1295 }
1296
1297 #[pyo3(signature = (data_type, identifier=None))]
1308 #[expect(clippy::needless_pass_by_value)]
1309 pub fn query_first_timestamp(
1310 &self,
1311 data_type: PyCatalogDataType,
1312 identifier: Option<String>,
1313 ) -> PyResult<Option<u64>> {
1314 let data_type = data_type.into_inner();
1315 self.inner
1316 .query_first_timestamp(&data_type, identifier.as_deref())
1317 .map_err(|e| PyIOError::new_err(format!("Failed to query first timestamp: {e}")))
1318 }
1319
1320 #[pyo3(signature = (data_type, identifier=None))]
1331 #[expect(clippy::needless_pass_by_value)]
1332 pub fn query_last_timestamp(
1333 &self,
1334 data_type: PyCatalogDataType,
1335 identifier: Option<String>,
1336 ) -> PyResult<Option<u64>> {
1337 let data_type = data_type.into_inner();
1338 self.inner
1339 .query_last_timestamp(&data_type, identifier.as_deref())
1340 .map_err(|e| PyIOError::new_err(format!("Failed to query last timestamp: {e}")))
1341 }
1342
1343 #[pyo3(signature = (data_type, identifier=None))]
1354 #[expect(clippy::needless_pass_by_value)]
1355 pub fn get_intervals(
1356 &self,
1357 data_type: PyCatalogDataType,
1358 identifier: Option<String>,
1359 ) -> PyResult<Vec<(u64, u64)>> {
1360 let data_type = data_type.into_inner();
1361 self.inner
1362 .get_intervals(&data_type, identifier.as_deref())
1363 .map_err(|e| PyIOError::new_err(format!("Failed to get intervals: {e}")))
1364 }
1365
1366 #[pyo3(signature = (data_type, identifiers=None, start=None, end=None, where_clause=None, files=None, optimize_file_loading=true))]
1368 #[expect(
1369 clippy::too_many_arguments,
1370 reason = "PyO3 signature mirrors the catalog query filters"
1371 )]
1372 pub fn query(
1373 &mut self,
1374 py: Python<'_>,
1375 #[gen_stub(override_type(type_repr = "model.NautilusDataType"))] data_type: &Bound<
1376 '_,
1377 PyAny,
1378 >,
1379 identifiers: Option<Vec<String>>,
1380 start: Option<u64>,
1381 end: Option<u64>,
1382 where_clause: Option<&str>,
1383 files: Option<Vec<String>>,
1384 optimize_file_loading: bool,
1385 ) -> PyResult<Vec<Py<PyAny>>> {
1386 let data_type = nautilus_data_type_from_py(data_type)?;
1387 let start_nanos = start.map(UnixNanos::from);
1388 let end_nanos = end.map(UnixNanos::from);
1389
1390 macro_rules! typed {
1391 ($type:ty) => {
1392 query_parquet_data::<$type>(
1393 &mut self.inner,
1394 identifiers,
1395 start,
1396 end,
1397 where_clause,
1398 files,
1399 optimize_file_loading,
1400 "Query failed",
1401 )?
1402 .into_iter()
1403 .map(Data::from)
1404 .collect::<Vec<Data>>()
1405 };
1406 }
1407
1408 let data = match data_type {
1409 NautilusDataType::QuoteTick => typed!(QuoteTick),
1410 NautilusDataType::TradeTick => typed!(TradeTick),
1411 NautilusDataType::Bar => typed!(Bar),
1412 NautilusDataType::OrderBookDelta => typed!(OrderBookDelta),
1413 NautilusDataType::OrderBookDepth => typed!(OrderBookDepth),
1414 NautilusDataType::IndexPriceUpdate => typed!(IndexPriceUpdate),
1415 NautilusDataType::MarkPriceUpdate => typed!(MarkPriceUpdate),
1416 NautilusDataType::FundingRateUpdate => typed!(FundingRateUpdate),
1417 NautilusDataType::OptionGreeks => typed!(OptionGreeks),
1418 NautilusDataType::InstrumentStatus => typed!(InstrumentStatus),
1419 NautilusDataType::InstrumentClose => typed!(InstrumentClose),
1420 NautilusDataType::Custom { type_name } => py
1421 .detach(|| {
1422 self.inner.query_custom_data_dynamic(
1423 &type_name,
1424 identifiers.as_deref(),
1425 start_nanos,
1426 end_nanos,
1427 where_clause,
1428 files.clone(),
1429 optimize_file_loading,
1430 )
1431 })
1432 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?,
1433 NautilusDataType::Instrument => {
1434 let instruments = py
1435 .detach(|| {
1436 self.inner.query_instruments_filtered_with_where(
1437 identifiers.as_deref(),
1438 start_nanos,
1439 end_nanos,
1440 where_clause,
1441 )
1442 })
1443 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
1444
1445 return instruments
1446 .into_iter()
1447 .map(|instrument| instrument_any_to_pyobject(py, instrument))
1448 .collect();
1449 }
1450 #[cfg(feature = "defi")]
1451 NautilusDataType::Defi => {
1452 return Err(to_pytype_err("Defi data is not supported by query"));
1453 }
1454 #[cfg(not(feature = "defi"))]
1455 #[allow(
1456 unreachable_patterns,
1457 reason = "DeFi variants can exist without this crate's defi feature"
1458 )]
1459 _ => return Err(to_pytype_err("Defi data is not supported by query")),
1460 };
1461
1462 let mut python_objects = Vec::new();
1463 for item in data {
1464 python_objects.push(data_to_pyobject(py, item)?);
1465 }
1466
1467 Ok(python_objects)
1468 }
1469
1470 #[pyo3(signature = (identifiers=None, start=None, end=None, where_clause=None))]
1485 pub fn query_quote_ticks(
1486 &mut self,
1487 identifiers: Option<Vec<String>>,
1488 start: Option<u64>,
1489 end: Option<u64>,
1490 where_clause: Option<&str>,
1491 ) -> PyResult<Vec<QuoteTick>> {
1492 query_parquet_data::<QuoteTick>(
1493 &mut self.inner,
1494 identifiers,
1495 start,
1496 end,
1497 where_clause,
1498 None,
1499 true,
1500 "Failed to query data",
1501 )
1502 }
1503
1504 #[pyo3(signature = (identifiers=None, start=None, end=None, where_clause=None))]
1519 pub fn query_trade_ticks(
1520 &mut self,
1521 identifiers: Option<Vec<String>>,
1522 start: Option<u64>,
1523 end: Option<u64>,
1524 where_clause: Option<&str>,
1525 ) -> PyResult<Vec<TradeTick>> {
1526 query_parquet_data::<TradeTick>(
1527 &mut self.inner,
1528 identifiers,
1529 start,
1530 end,
1531 where_clause,
1532 None,
1533 true,
1534 "Failed to query data",
1535 )
1536 }
1537
1538 #[pyo3(signature = (identifiers=None, start=None, end=None, where_clause=None))]
1553 pub fn query_order_book_deltas(
1554 &mut self,
1555 identifiers: Option<Vec<String>>,
1556 start: Option<u64>,
1557 end: Option<u64>,
1558 where_clause: Option<&str>,
1559 ) -> PyResult<Vec<OrderBookDelta>> {
1560 query_parquet_data::<OrderBookDelta>(
1561 &mut self.inner,
1562 identifiers,
1563 start,
1564 end,
1565 where_clause,
1566 None,
1567 true,
1568 "Failed to query data",
1569 )
1570 }
1571
1572 #[pyo3(signature = (identifiers=None, start=None, end=None, where_clause=None))]
1587 pub fn query_bars(
1588 &mut self,
1589 identifiers: Option<Vec<String>>,
1590 start: Option<u64>,
1591 end: Option<u64>,
1592 where_clause: Option<&str>,
1593 ) -> PyResult<Vec<Bar>> {
1594 query_parquet_data::<Bar>(
1595 &mut self.inner,
1596 identifiers,
1597 start,
1598 end,
1599 where_clause,
1600 None,
1601 true,
1602 "Failed to query data",
1603 )
1604 }
1605
1606 #[pyo3(signature = (identifiers=None, start=None, end=None, where_clause=None))]
1619 pub fn query_order_book_depths(
1620 &mut self,
1621 identifiers: Option<Vec<String>>,
1622 start: Option<u64>,
1623 end: Option<u64>,
1624 where_clause: Option<&str>,
1625 ) -> PyResult<Vec<OrderBookDepth>> {
1626 query_parquet_data::<OrderBookDepth>(
1627 &mut self.inner,
1628 identifiers,
1629 start,
1630 end,
1631 where_clause,
1632 None,
1633 true,
1634 "Failed to query data",
1635 )
1636 }
1637
1638 #[pyo3(signature = (identifiers=None, start=None, end=None, where_clause=None))]
1651 pub fn query_mark_price_updates(
1652 &mut self,
1653 identifiers: Option<Vec<String>>,
1654 start: Option<u64>,
1655 end: Option<u64>,
1656 where_clause: Option<&str>,
1657 ) -> PyResult<Vec<MarkPriceUpdate>> {
1658 query_parquet_data::<MarkPriceUpdate>(
1659 &mut self.inner,
1660 identifiers,
1661 start,
1662 end,
1663 where_clause,
1664 None,
1665 true,
1666 "Failed to query data",
1667 )
1668 }
1669
1670 #[pyo3(signature = (identifiers=None, start=None, end=None, where_clause=None))]
1683 pub fn query_index_price_updates(
1684 &mut self,
1685 identifiers: Option<Vec<String>>,
1686 start: Option<u64>,
1687 end: Option<u64>,
1688 where_clause: Option<&str>,
1689 ) -> PyResult<Vec<IndexPriceUpdate>> {
1690 query_parquet_data::<IndexPriceUpdate>(
1691 &mut self.inner,
1692 identifiers,
1693 start,
1694 end,
1695 where_clause,
1696 None,
1697 true,
1698 "Failed to query data",
1699 )
1700 }
1701
1702 #[pyo3(signature = (identifiers=None, start=None, end=None, where_clause=None))]
1704 pub fn query_option_greeks(
1705 &mut self,
1706 identifiers: Option<Vec<String>>,
1707 start: Option<u64>,
1708 end: Option<u64>,
1709 where_clause: Option<&str>,
1710 ) -> PyResult<Vec<OptionGreeks>> {
1711 query_parquet_data::<OptionGreeks>(
1712 &mut self.inner,
1713 identifiers,
1714 start,
1715 end,
1716 where_clause,
1717 None,
1718 true,
1719 "Failed to query data",
1720 )
1721 }
1722
1723 pub fn list_data_types(&self) -> PyResult<Vec<PyCatalogDataType>> {
1729 self.inner
1730 .list_data_types()
1731 .map(|data_types| data_types.into_iter().map(PyCatalogDataType::new).collect())
1732 .map_err(|e| PyIOError::new_err(format!("Failed to list data types: {e}")))
1733 }
1734
1735 pub fn list_live_runs(&self) -> PyResult<Vec<String>> {
1741 self.inner
1742 .list_live_runs()
1743 .map_err(|e| PyIOError::new_err(format!("Failed to list live runs: {e}")))
1744 }
1745
1746 pub fn list_backtest_runs(&self) -> PyResult<Vec<String>> {
1752 self.inner
1753 .list_backtest_runs()
1754 .map_err(|e| PyIOError::new_err(format!("Failed to list backtest runs: {e}")))
1755 }
1756
1757 pub fn list_backtests(&self) -> PyResult<Vec<String>> {
1759 self.inner
1760 .list_backtest_runs()
1761 .map_err(|e| PyIOError::new_err(format!("Failed to list backtests: {e}")))
1762 }
1763
1764 #[pyo3(signature = (instance_id))]
1774 pub fn read_live_run(&self, py: Python<'_>, instance_id: &str) -> PyResult<Vec<Py<PyAny>>> {
1775 let data = self
1776 .inner
1777 .read_live_run(instance_id)
1778 .map_err(|e| PyIOError::new_err(format!("Failed to read live run: {e}")))?;
1779
1780 let mut python_objects = Vec::new();
1781 for item in data {
1782 python_objects.push(data_to_pyobject(py, item)?);
1783 }
1784
1785 Ok(python_objects)
1786 }
1787
1788 #[pyo3(signature = (instance_id))]
1798 pub fn read_backtest(&self, py: Python<'_>, instance_id: &str) -> PyResult<Vec<Py<PyAny>>> {
1799 let data = self
1800 .inner
1801 .read_backtest(instance_id)
1802 .map_err(|e| PyIOError::new_err(format!("Failed to read backtest: {e}")))?;
1803
1804 let mut python_objects = Vec::new();
1805 for item in data {
1806 python_objects.push(data_to_pyobject(py, item)?);
1807 }
1808
1809 Ok(python_objects)
1810 }
1811
1812 #[pyo3(signature = (instance_id, data_type, environment=Environment::Backtest, identifiers=None, use_ts_event_for_ts_init=false))]
1849 #[expect(clippy::needless_pass_by_value)]
1850 pub fn convert_stream_to_data(
1851 &mut self,
1852 instance_id: &str,
1853 data_type: PyCatalogDataType,
1854 environment: Environment,
1855 identifiers: Option<Vec<String>>,
1856 use_ts_event_for_ts_init: bool,
1857 ) -> PyResult<()> {
1858 let data_type = data_type.into_inner();
1859
1860 self.inner
1861 .convert_stream_to_data(
1862 instance_id,
1863 &data_type,
1864 environment,
1865 identifiers.as_deref(),
1866 use_ts_event_for_ts_init,
1867 )
1868 .map_err(|e| PyIOError::new_err(format!("Failed to convert stream to data: {e}")))
1869 }
1870
1871 #[pyo3(signature = (data_type, identifiers=None, start=None, end=None, where_clause=None))]
1873 #[expect(clippy::needless_pass_by_value)]
1874 pub fn query_custom_data(
1875 &mut self,
1876 py: Python<'_>,
1877 #[gen_stub(override_type(type_repr = "model.NautilusDataType"))] data_type: &Bound<
1878 '_,
1879 PyAny,
1880 >,
1881 identifiers: Option<Vec<String>>,
1882 start: Option<u64>,
1883 end: Option<u64>,
1884 where_clause: Option<&str>,
1885 ) -> PyResult<Vec<Py<PyAny>>> {
1886 let NautilusDataType::Custom { type_name } = nautilus_data_type_from_py(data_type)? else {
1887 return Err(to_pytype_err("data_type must be a custom NautilusDataType"));
1888 };
1889
1890 let start_nanos = start.map(UnixNanos::from);
1891 let end_nanos = end.map(UnixNanos::from);
1892
1893 let data = py
1894 .detach(|| {
1895 self.inner.query_custom_data_dynamic(
1896 &type_name,
1897 identifiers.as_deref(),
1898 start_nanos,
1899 end_nanos,
1900 where_clause,
1901 None,
1902 true,
1903 )
1904 })
1905 .map_err(|e| PyIOError::new_err(format!("Failed to query custom data: {e}")))?;
1906
1907 let mut python_objects = Vec::new();
1908
1909 for item in data {
1910 let py_obj: Py<PyAny> = match item {
1911 Data::Custom(custom) => Py::new(py, custom)?.into_any(),
1912 _ => return Err(PyIOError::new_err("Expected custom data")),
1913 };
1914
1915 python_objects.push(py_obj);
1916 }
1917
1918 Ok(python_objects)
1919 }
1920}
1921
1922fn catalog_compression_from_code(code: u8) -> PyResult<CatalogCompression> {
1925 match code {
1926 0 => Ok(CatalogCompression::Uncompressed),
1927 1 => Ok(CatalogCompression::Snappy),
1928 2 => Ok(CatalogCompression::Gzip),
1929 4 => Ok(CatalogCompression::Brotli),
1930 5 => Ok(CatalogCompression::Lz4),
1931 6 => Ok(CatalogCompression::Zstd),
1932 _ => Err(to_pyvalue_err(format!(
1933 "Invalid compression code {code}, expected one of 0 (UNCOMPRESSED), 1 (SNAPPY), \
1934 2 (GZIP), 4 (BROTLI), 5 (LZ4_RAW), or 6 (ZSTD)"
1935 ))),
1936 }
1937}
1938
1939#[cfg(test)]
1940mod tests {
1941 use rstest::rstest;
1942
1943 use super::*;
1944
1945 #[rstest]
1946 #[case::uncompressed(0, CatalogCompression::Uncompressed)]
1947 #[case::snappy(1, CatalogCompression::Snappy)]
1948 #[case::gzip(2, CatalogCompression::Gzip)]
1949 #[case::brotli(4, CatalogCompression::Brotli)]
1950 #[case::lz4(5, CatalogCompression::Lz4)]
1951 #[case::zstd(6, CatalogCompression::Zstd)]
1952 fn catalog_compression_from_code_maps_parquet_codes(
1953 #[case] code: u8,
1954 #[case] expected: CatalogCompression,
1955 ) {
1956 assert_eq!(catalog_compression_from_code(code).unwrap(), expected);
1957 }
1958}