Skip to main content

nautilus_coinapi/python/
mod.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//! Python bindings from [PyO3](https://pyo3.rs).
17
18pub mod config;
19pub mod csv;
20pub mod factories;
21
22use nautilus_common::factories::{ClientConfig, DataClientFactory};
23use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
24use nautilus_system::get_global_pyo3_registry;
25use pyo3::prelude::*;
26
27use crate::{
28    common::consts::{COINAPI, COINAPI_CLIENT_ID},
29    config::{CoinApiDataClientConfig, CoinApiInstrumentSelection},
30    factories::CoinApiDataClientFactory,
31};
32
33#[expect(clippy::needless_pass_by_value)]
34fn extract_coinapi_data_factory(
35    py: Python<'_>,
36    factory: Py<PyAny>,
37) -> PyResult<Box<dyn DataClientFactory>> {
38    match factory.extract::<CoinApiDataClientFactory>(py) {
39        Ok(f) => Ok(Box::new(f)),
40        Err(e) => Err(to_pyvalue_err(format!(
41            "Failed to extract CoinApiDataClientFactory: {e}"
42        ))),
43    }
44}
45
46#[expect(clippy::needless_pass_by_value)]
47fn extract_coinapi_data_config(
48    py: Python<'_>,
49    config: Py<PyAny>,
50) -> PyResult<Box<dyn ClientConfig>> {
51    match config.extract::<CoinApiDataClientConfig>(py) {
52        Ok(c) => Ok(Box::new(c)),
53        Err(e) => Err(to_pyvalue_err(format!(
54            "Failed to extract CoinApiDataClientConfig: {e}"
55        ))),
56    }
57}
58
59/// CoinAPI Python module exposed as `nautilus_trader._libnautilus.coinapi`.
60///
61/// # Errors
62///
63/// Returns an error if adding module attributes or registering an extractor fails.
64#[pymodule]
65pub fn coinapi(m: &Bound<'_, PyModule>) -> PyResult<()> {
66    m.add(stringify!(COINAPI), COINAPI)?;
67    m.add(stringify!(COINAPI_CLIENT_ID), *COINAPI_CLIENT_ID)?;
68    m.add_class::<CoinApiDataClientConfig>()?;
69    m.add_class::<CoinApiDataClientFactory>()?;
70    m.add_class::<CoinApiInstrumentSelection>()?;
71    m.add_class::<crate::csv::CoinApiOhlcvLoader>()?;
72    m.add_class::<csv::CoinApiDeltaStreamIterator>()?;
73    m.add_class::<csv::CoinApiQuoteStreamIterator>()?;
74    m.add_class::<csv::CoinApiTradeStreamIterator>()?;
75    m.add_function(wrap_pyfunction!(csv::py_load_coinapi_deltas, m)?)?;
76    m.add_function(wrap_pyfunction!(csv::py_load_coinapi_quotes, m)?)?;
77    m.add_function(wrap_pyfunction!(csv::py_load_coinapi_trades, m)?)?;
78    m.add_function(wrap_pyfunction!(csv::py_stream_coinapi_deltas, m)?)?;
79    m.add_function(wrap_pyfunction!(csv::py_stream_coinapi_quotes, m)?)?;
80    m.add_function(wrap_pyfunction!(csv::py_stream_coinapi_trades, m)?)?;
81
82    let registry = get_global_pyo3_registry();
83
84    if let Err(e) =
85        registry.register_factory_extractor(COINAPI.to_string(), extract_coinapi_data_factory)
86    {
87        return Err(to_pyruntime_err(format!(
88            "Failed to register CoinAPI data factory extractor: {e}"
89        )));
90    }
91
92    if let Err(e) = registry.register_config_extractor(
93        "CoinApiDataClientConfig".to_string(),
94        extract_coinapi_data_config,
95    ) {
96        return Err(to_pyruntime_err(format!(
97            "Failed to register CoinAPI data config extractor: {e}"
98        )));
99    }
100
101    Ok(())
102}
103
104#[cfg(test)]
105mod tests {
106    use nautilus_model::identifiers::ClientId;
107    use pyo3::prelude::*;
108    use rstest::rstest;
109
110    #[rstest]
111    fn test_module_registration() {
112        Python::initialize();
113        Python::attach(|py| {
114            let module = pyo3::wrap_pymodule!(super::coinapi)(py);
115            let module = module.bind(py);
116
117            assert_eq!(module.name().unwrap(), "coinapi");
118            assert_eq!(
119                module
120                    .getattr("COINAPI")
121                    .unwrap()
122                    .extract::<String>()
123                    .unwrap(),
124                "COINAPI",
125            );
126            assert_eq!(
127                module
128                    .getattr("COINAPI_CLIENT_ID")
129                    .unwrap()
130                    .extract::<ClientId>()
131                    .unwrap(),
132                ClientId::from("COINAPI"),
133            );
134
135            for name in [
136                "CoinApiOhlcvLoader",
137                "load_coinapi_deltas",
138                "load_coinapi_quotes",
139                "load_coinapi_trades",
140                "stream_coinapi_deltas",
141                "stream_coinapi_quotes",
142                "stream_coinapi_trades",
143            ] {
144                assert!(module.hasattr(name).unwrap(), "missing {name}");
145            }
146        });
147    }
148}