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}