Skip to main content

nautilus_persistence/
config.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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
16//! Configuration types for persistence backends.
17
18use ahash::AHashMap;
19#[cfg(feature = "python")]
20use nautilus_core::python::to_pyvalue_err;
21use serde::{Deserialize, Serialize};
22
23use crate::backend::catalog::ParquetDataCatalog;
24
25/// Configuration for an existing Parquet data catalog.
26#[cfg_attr(
27    feature = "python",
28    pyo3::pyclass(module = "nautilus_trader.persistence", from_py_object, frozen, eq)
29)]
30#[cfg_attr(
31    feature = "python",
32    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.persistence")
33)]
34#[cfg_attr(
35    feature = "python",
36    expect(
37        clippy::unsafe_derive_deserialize,
38        reason = "config deserializes plain fields; unsafe methods come from generated PyO3 integration"
39    )
40)]
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct DataCatalogConfig {
44    /// The path to the data catalog.
45    pub path: String,
46    /// The filesystem protocol for the data catalog.
47    pub fs_protocol: Option<String>,
48    /// Storage options for the Rust object-store backend.
49    pub fs_rust_storage_options: Option<AHashMap<String, String>>,
50    /// The catalog name used by the data engine.
51    pub name: Option<String>,
52}
53
54impl DataCatalogConfig {
55    /// Creates a new [`DataCatalogConfig`] instance.
56    #[must_use]
57    pub const fn new(
58        path: String,
59        fs_protocol: Option<String>,
60        fs_rust_storage_options: Option<AHashMap<String, String>>,
61        name: Option<String>,
62    ) -> Self {
63        Self {
64            path,
65            fs_protocol,
66            fs_rust_storage_options,
67            name,
68        }
69    }
70
71    /// Creates the configured [`ParquetDataCatalog`].
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if the configured URI or object-store options are invalid.
76    pub fn create_catalog(&self) -> anyhow::Result<ParquetDataCatalog> {
77        let uri = match self.fs_protocol.as_deref() {
78            Some("file") => self.path.clone(),
79            Some(protocol) if !self.path.contains("://") => {
80                format!("{protocol}://{}", self.path)
81            }
82            _ => self.path.clone(),
83        };
84
85        ParquetDataCatalog::from_uri(&uri, self.fs_rust_storage_options.clone(), None, None, None)
86    }
87}
88
89#[cfg(feature = "python")]
90#[pyo3_stub_gen::derive::gen_stub_pymethods]
91#[pyo3::pymethods]
92impl DataCatalogConfig {
93    /// Creates a configuration for an existing Parquet data catalog.
94    #[new]
95    #[pyo3(signature = (path, fs_protocol=None, fs_rust_storage_options=None, name=None))]
96    fn py_new(
97        path: String,
98        fs_protocol: Option<String>,
99        fs_rust_storage_options: Option<std::collections::HashMap<String, String>>,
100        name: Option<String>,
101    ) -> pyo3::PyResult<Self> {
102        if path.trim().is_empty() {
103            return Err(to_pyvalue_err("path must not be empty"));
104        }
105
106        if fs_protocol
107            .as_ref()
108            .is_some_and(|value| value.trim().is_empty())
109        {
110            return Err(to_pyvalue_err("fs_protocol must not be empty"));
111        }
112
113        if name.as_ref().is_some_and(|value| value.trim().is_empty()) {
114            return Err(to_pyvalue_err("name must not be empty"));
115        }
116
117        Ok(Self::new(
118            path,
119            fs_protocol,
120            fs_rust_storage_options.map(|values| values.into_iter().collect()),
121            name,
122        ))
123    }
124
125    #[getter]
126    fn path(&self) -> &str {
127        &self.path
128    }
129
130    #[getter]
131    fn fs_protocol(&self) -> Option<&str> {
132        self.fs_protocol.as_deref()
133    }
134
135    #[getter]
136    fn name(&self) -> Option<&str> {
137        self.name.as_deref()
138    }
139
140    /// Returns the sorted Rust storage-option keys without exposing secret values.
141    #[getter]
142    fn fs_rust_storage_option_keys(&self) -> Option<Vec<String>> {
143        self.fs_rust_storage_options.as_ref().map(|options| {
144            let mut keys = options.keys().cloned().collect::<Vec<_>>();
145            keys.sort_unstable();
146            keys
147        })
148    }
149
150    fn __repr__(&self) -> String {
151        format!(
152            "DataCatalogConfig(path='{}', fs_protocol={:?}, name={:?})",
153            self.path, self.fs_protocol, self.name
154        )
155    }
156}