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