Skip to main content

nautilus_infrastructure/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
18#[cfg(feature = "redis")]
19pub mod redis;
20
21#[cfg(feature = "postgres")]
22pub mod sql;
23
24use pyo3::{prelude::*, pymodule};
25
26/// Python module initializer for the `infrastructure` package.
27///
28/// # Errors
29///
30/// Returns a `PyErr` if the module initialization fails, e.g., when adding classes to the module.
31#[pymodule]
32pub fn infrastructure(_: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
33    #[cfg(not(any(feature = "redis", feature = "postgres")))]
34    let _ = m;
35
36    #[cfg(feature = "redis")]
37    m.add_class::<crate::redis::cache::RedisCacheConfig>()?;
38    #[cfg(feature = "redis")]
39    m.add_class::<crate::redis::cache::RedisCacheDatabase>()?;
40    #[cfg(feature = "redis")]
41    m.add_class::<redis::msgbus::PyRedisMessageBusBacking>()?;
42    #[cfg(feature = "redis")]
43    m.add_class::<redis::msgbus::PyRedisMessageBusFactory>()?;
44    #[cfg(feature = "redis")]
45    m.add_class::<crate::redis::msgbus::RedisMessageBusConfig>()?;
46    #[cfg(feature = "redis")]
47    redis::msgbus::register_redis_msgbus_factory()?;
48    #[cfg(feature = "redis")]
49    redis::cache::register_redis_cache_database_factory()?;
50    #[cfg(feature = "postgres")]
51    m.add_class::<crate::sql::cache::PostgresCacheConfig>()?;
52    #[cfg(feature = "postgres")]
53    m.add_class::<crate::sql::cache::PostgresCacheDatabase>()?;
54    #[cfg(feature = "postgres")]
55    m.add_class::<crate::sql::pg::PostgresConnectOptions>()?;
56    #[cfg(feature = "postgres")]
57    sql::cache::register_postgres_cache_database_factory()?;
58    Ok(())
59}
60
61#[cfg(all(test, any(feature = "redis", feature = "postgres")))]
62mod tests {
63    use nautilus_common::python::cache::get_global_cache_database_factory_registry;
64    #[cfg(feature = "redis")]
65    use nautilus_common::python::msgbus::get_global_msgbus_factory_registry;
66    use pyo3::PyRef;
67    use rstest::rstest;
68
69    use super::*;
70
71    #[cfg(feature = "redis")]
72    #[rstest]
73    fn test_infrastructure_module_extracts_redis_cache_database_factory() {
74        Python::initialize();
75        Python::attach(|py| {
76            let module = PyModule::new(py, "infrastructure").unwrap();
77
78            infrastructure(py, &module).unwrap();
79
80            let config = module
81                .getattr("RedisCacheConfig")
82                .unwrap()
83                .call1((
84                    "redis.example.com",
85                    6380,
86                    "user",
87                    "secret",
88                    true,
89                    7,
90                    8,
91                    9,
92                    3,
93                    10,
94                    4,
95                ))
96                .unwrap();
97            {
98                let config = config
99                    .extract::<PyRef<crate::redis::cache::RedisCacheConfig>>()
100                    .unwrap();
101
102                assert_eq!(config.host.as_deref(), Some("redis.example.com"));
103                assert_eq!(config.port, Some(6380));
104                assert_eq!(config.username.as_deref(), Some("user"));
105                assert_eq!(config.password.as_deref(), Some("secret"));
106                assert!(config.ssl);
107                assert_eq!(config.connection_timeout, 7);
108                assert_eq!(config.response_timeout, 8);
109                assert_eq!(config.number_of_retries, 9);
110                assert_eq!(config.exponent_base, 3);
111                assert_eq!(config.max_delay, 10);
112                assert_eq!(config.factor, 4);
113            }
114            let factory = get_global_cache_database_factory_registry()
115                .extract(py, config.unbind())
116                .unwrap();
117            let debug = format!("{factory:?}");
118
119            assert!(debug.contains("redis.example.com"));
120            assert!(debug.contains("password: Some(\"***\")"));
121            assert!(!debug.contains("secret"));
122        });
123    }
124
125    #[cfg(feature = "postgres")]
126    #[rstest]
127    fn test_infrastructure_module_extracts_postgres_cache_database_factory() {
128        Python::initialize();
129        Python::attach(|py| {
130            let module = PyModule::new(py, "infrastructure").unwrap();
131
132            infrastructure(py, &module).unwrap();
133
134            let config = module
135                .getattr("PostgresCacheConfig")
136                .unwrap()
137                .call1(("postgres.example.com", 5433, "user", "secret", "nautilus"))
138                .unwrap();
139            {
140                let config = config
141                    .extract::<PyRef<crate::sql::cache::PostgresCacheConfig>>()
142                    .unwrap();
143
144                assert_eq!(config.host.as_deref(), Some("postgres.example.com"));
145                assert_eq!(config.port, Some(5433));
146                assert_eq!(config.username.as_deref(), Some("user"));
147                assert_eq!(config.password.as_deref(), Some("secret"));
148                assert_eq!(config.database.as_deref(), Some("nautilus"));
149            }
150            let factory = get_global_cache_database_factory_registry()
151                .extract(py, config.unbind())
152                .unwrap();
153            let debug = format!("{factory:?}");
154
155            assert!(debug.contains("postgres.example.com"));
156            assert!(debug.contains("password: Some(\"***\")"));
157            assert!(!debug.contains("secret"));
158        });
159    }
160
161    #[cfg(feature = "redis")]
162    #[rstest]
163    fn test_infrastructure_module_exports_redis_message_bus_types() {
164        Python::initialize();
165        Python::attach(|py| {
166            let module = PyModule::new(py, "infrastructure").unwrap();
167
168            infrastructure(py, &module).unwrap();
169
170            assert!(module.getattr("RedisMessageBusBacking").is_ok());
171            assert!(module.getattr("RedisMessageBusConfig").is_ok());
172            assert!(module.getattr("RedisMessageBusFactory").is_ok());
173
174            let config = module
175                .getattr("RedisMessageBusConfig")
176                .unwrap()
177                .call1((
178                    "redis.example.com",
179                    6380,
180                    "user",
181                    "secret",
182                    true,
183                    7,
184                    8,
185                    9,
186                    3,
187                    10,
188                    4,
189                ))
190                .unwrap();
191            {
192                let config = config
193                    .extract::<PyRef<crate::redis::msgbus::RedisMessageBusConfig>>()
194                    .unwrap();
195
196                assert_eq!(config.host.as_deref(), Some("redis.example.com"));
197                assert_eq!(config.port, Some(6380));
198                assert_eq!(config.username.as_deref(), Some("user"));
199                assert_eq!(config.password.as_deref(), Some("secret"));
200                assert!(config.ssl);
201                assert_eq!(config.connection_timeout, 7);
202                assert_eq!(config.response_timeout, 8);
203                assert_eq!(config.number_of_retries, 9);
204                assert_eq!(config.exponent_base, 3);
205                assert_eq!(config.max_delay, 10);
206                assert_eq!(config.factor, 4);
207            }
208            let direct_factory = get_global_msgbus_factory_registry()
209                .extract(py, config.unbind())
210                .unwrap();
211            let debug = format!("{direct_factory:?}");
212
213            assert!(debug.contains("redis.example.com"));
214            assert!(debug.contains("password: Some(\"***\")"));
215            assert!(!debug.contains("secret"));
216
217            let config = module
218                .getattr("RedisMessageBusConfig")
219                .unwrap()
220                .call1(("redis.example.com", 6380, "user", "secret"))
221                .unwrap();
222            let factory = module
223                .getattr("RedisMessageBusFactory")
224                .unwrap()
225                .call1((config,))
226                .unwrap()
227                .unbind();
228            let compatibility_factory = get_global_msgbus_factory_registry()
229                .extract(py, factory)
230                .unwrap();
231            let debug = format!("{compatibility_factory:?}");
232
233            assert!(debug.contains("redis.example.com"));
234            assert!(debug.contains("password: Some(\"***\")"));
235            assert!(!debug.contains("secret"));
236
237            let second_module = PyModule::new(py, "infrastructure").unwrap();
238            infrastructure(py, &second_module).unwrap();
239            assert!(second_module.getattr("RedisMessageBusConfig").is_ok());
240            assert!(second_module.getattr("RedisMessageBusFactory").is_ok());
241        });
242    }
243}