Skip to main content

nautilus_system/python/
registry.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//! PyO3 registry system for generic trait object extraction.
17
18use std::collections::HashMap;
19
20use nautilus_common::factories::{
21    ClientConfig, DataClientFactory, ExecutionClientFactory, SimulatedExecutionClientFactory,
22};
23use nautilus_core::python::to_pynotimplemented_err;
24use parking_lot::Mutex;
25use pyo3::prelude::*;
26
27/// Function type for extracting a `Py<PyAny>` factory to a boxed `DataClientFactory` trait object.
28pub type FactoryExtractor =
29    fn(py: Python<'_>, factory: Py<PyAny>) -> PyResult<Box<dyn DataClientFactory>>;
30
31/// Function type for extracting a `Py<PyAny>` factory to a boxed `ExecutionClientFactory` trait object.
32pub type ExecutionFactoryExtractor =
33    fn(py: Python<'_>, factory: Py<PyAny>) -> PyResult<Box<dyn ExecutionClientFactory>>;
34
35/// Function type for extracting a `Py<PyAny>` factory to a boxed `SimulatedExecutionClientFactory` trait object.
36pub type SimulatedExecutionFactoryExtractor =
37    fn(py: Python<'_>, factory: Py<PyAny>) -> PyResult<Box<dyn SimulatedExecutionClientFactory>>;
38
39/// Function type for extracting a `Py<PyAny>` config to a boxed `ClientConfig` trait object.
40pub type ConfigExtractor = fn(py: Python<'_>, config: Py<PyAny>) -> PyResult<Box<dyn ClientConfig>>;
41
42/// Registry for PyO3 factory and config extractors.
43///
44/// This allows each adapter to register its own extraction logic for converting
45/// `Py<PyAny>s` to boxed trait objects without requiring the live crate to know
46/// about specific implementations.
47#[derive(Debug)]
48pub struct FactoryRegistry {
49    factory_extractors: Mutex<HashMap<String, FactoryExtractor>>,
50    exec_factory_extractors: Mutex<HashMap<String, ExecutionFactoryExtractor>>,
51    sim_exec_factory_extractors: Mutex<HashMap<String, SimulatedExecutionFactoryExtractor>>,
52    config_extractors_by_type: Mutex<HashMap<String, ConfigExtractor>>,
53}
54
55impl FactoryRegistry {
56    /// Creates a new empty registry.
57    #[must_use]
58    pub fn new() -> Self {
59        Self {
60            factory_extractors: Mutex::new(HashMap::new()),
61            exec_factory_extractors: Mutex::new(HashMap::new()),
62            sim_exec_factory_extractors: Mutex::new(HashMap::new()),
63            config_extractors_by_type: Mutex::new(HashMap::new()),
64        }
65    }
66
67    /// Registers a factory extractor for a specific factory name.
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if a factory with the same name is already registered.
72    pub fn register_factory_extractor(
73        &self,
74        name: String,
75        extractor: FactoryExtractor,
76    ) -> anyhow::Result<()> {
77        let mut extractors = self.factory_extractors.lock();
78
79        if extractors.contains_key(&name) {
80            anyhow::bail!("Factory extractor '{name}' is already registered");
81        }
82        extractors.insert(name, extractor);
83        Ok(())
84    }
85
86    /// Registers a config extractor for a specific config type name.
87    ///
88    /// # Errors
89    ///
90    /// Returns an error if a config with the same type name is already registered.
91    pub fn register_config_extractor(
92        &self,
93        type_name: String,
94        extractor: ConfigExtractor,
95    ) -> anyhow::Result<()> {
96        let mut extractors = self.config_extractors_by_type.lock();
97
98        if extractors.contains_key(&type_name) {
99            anyhow::bail!("Config extractor '{type_name}' is already registered");
100        }
101
102        extractors.insert(type_name, extractor);
103        Ok(())
104    }
105
106    /// Registers an execution factory extractor for a specific factory name.
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if a factory with the same name is already registered.
111    pub fn register_exec_factory_extractor(
112        &self,
113        name: String,
114        extractor: ExecutionFactoryExtractor,
115    ) -> anyhow::Result<()> {
116        let mut extractors = self.exec_factory_extractors.lock();
117
118        if extractors.contains_key(&name) {
119            anyhow::bail!("Execution factory extractor '{name}' is already registered");
120        }
121        extractors.insert(name, extractor);
122        Ok(())
123    }
124
125    /// Registers a simulated execution factory extractor for a specific factory name.
126    ///
127    /// # Errors
128    ///
129    /// Returns an error if a factory with the same name is already registered.
130    pub fn register_sim_exec_factory_extractor(
131        &self,
132        name: String,
133        extractor: SimulatedExecutionFactoryExtractor,
134    ) -> anyhow::Result<()> {
135        let mut extractors = self.sim_exec_factory_extractors.lock();
136
137        if extractors.contains_key(&name) {
138            anyhow::bail!("Simulated execution factory extractor '{name}' is already registered");
139        }
140        extractors.insert(name, extractor);
141        Ok(())
142    }
143
144    /// Extracts a `Py<PyAny>` factory to a boxed `DataClientFactory` trait object.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if no extractor is registered for the factory type or extraction fails.
149    pub fn extract_factory(
150        &self,
151        py: Python<'_>,
152        factory: Py<PyAny>,
153    ) -> PyResult<Box<dyn DataClientFactory>> {
154        // Get the factory name to find the appropriate extractor
155        let factory_name = factory
156            .getattr(py, "name")?
157            .call0(py)?
158            .extract::<String>(py)?;
159
160        let extractors = self.factory_extractors.lock();
161        if let Some(extractor) = extractors.get(&factory_name) {
162            extractor(py, factory)
163        } else {
164            Err(to_pynotimplemented_err(format!(
165                "No factory extractor registered for '{factory_name}'"
166            )))
167        }
168    }
169
170    /// Extracts a `Py<PyAny>` factory to a boxed `ExecutionClientFactory` trait object.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if no extractor is registered for the factory type or extraction fails.
175    pub fn extract_exec_factory(
176        &self,
177        py: Python<'_>,
178        factory: Py<PyAny>,
179    ) -> PyResult<Box<dyn ExecutionClientFactory>> {
180        let factory_name = factory
181            .getattr(py, "name")?
182            .call0(py)?
183            .extract::<String>(py)?;
184
185        let extractors = self.exec_factory_extractors.lock();
186        if let Some(extractor) = extractors.get(&factory_name) {
187            extractor(py, factory)
188        } else {
189            Err(to_pynotimplemented_err(format!(
190                "No execution factory extractor registered for '{factory_name}'"
191            )))
192        }
193    }
194
195    /// Extracts a `Py<PyAny>` factory to a boxed `SimulatedExecutionClientFactory` trait object.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if no extractor is registered for the factory type or extraction fails.
200    pub fn extract_sim_exec_factory(
201        &self,
202        py: Python<'_>,
203        factory: Py<PyAny>,
204    ) -> PyResult<Box<dyn SimulatedExecutionClientFactory>> {
205        let factory_name = factory
206            .getattr(py, "name")?
207            .call0(py)?
208            .extract::<String>(py)?;
209
210        let extractors = self.sim_exec_factory_extractors.lock();
211
212        if let Some(extractor) = extractors.get(&factory_name) {
213            extractor(py, factory)
214        } else {
215            Err(to_pynotimplemented_err(format!(
216                "No simulated execution factory extractor registered for '{factory_name}'"
217            )))
218        }
219    }
220
221    /// Extracts a `Py<PyAny>` config to a boxed `ClientConfig` trait object.
222    ///
223    /// # Errors
224    ///
225    /// Returns an error if no extractor is registered for the config type or extraction fails.
226    pub fn extract_config(
227        &self,
228        py: Python<'_>,
229        config: Py<PyAny>,
230    ) -> PyResult<Box<dyn ClientConfig>> {
231        // Get the config class name to find the appropriate extractor
232        let config_type_name = config
233            .getattr(py, "__class__")?
234            .getattr(py, "__name__")?
235            .extract::<String>(py)?;
236
237        let extractors = self.config_extractors_by_type.lock();
238        if let Some(extractor) = extractors.get(&config_type_name) {
239            extractor(py, config)
240        } else {
241            Err(to_pynotimplemented_err(format!(
242                "No config extractor registered for '{config_type_name}'"
243            )))
244        }
245    }
246}
247
248impl Default for FactoryRegistry {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254/// Global PyO3 registry instance.
255static GLOBAL_PYO3_REGISTRY: std::sync::LazyLock<FactoryRegistry> =
256    std::sync::LazyLock::new(FactoryRegistry::new);
257
258/// Gets a reference to the global PyO3 registry.
259#[must_use]
260pub fn get_global_pyo3_registry() -> &'static FactoryRegistry {
261    &GLOBAL_PYO3_REGISTRY
262}