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