nautilus_core/python/mod.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2025 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 and interoperability built using [`PyO3`](https://pyo3.rs).
17//!
18//! This sub-module groups together the Rust code that is *only* required when compiling the
19//! `python` feature flag. It provides thin adapters so that NautilusTrader functionality can be
20//! consumed from the `nautilus_trader` Python package without sacrificing type-safety or
21//! performance.
22
23pub mod casing;
24pub mod datetime;
25pub mod enums;
26pub mod parsing;
27pub mod serialization;
28pub mod uuid;
29pub mod version;
30
31use pyo3::{
32 conversion::IntoPyObjectExt,
33 exceptions::{PyRuntimeError, PyTypeError, PyValueError},
34 prelude::*,
35 types::PyString,
36 wrap_pyfunction,
37};
38
39use crate::{
40 UUID4,
41 consts::{NAUTILUS_USER_AGENT, NAUTILUS_VERSION},
42 datetime::{
43 MILLISECONDS_IN_SECOND, NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND,
44 NANOSECONDS_IN_SECOND,
45 },
46};
47
48/// Safely clones a Python object by acquiring the GIL and properly managing reference counts.
49///
50/// This function exists to break reference cycles between Rust and Python that can occur
51/// when using `Arc<PyObject>` in callback-holding structs. The original design wrapped
52/// Python callbacks in `Arc` for thread-safe sharing, but this created circular references:
53///
54/// 1. Rust `Arc` holds Python objects → increases Python reference count.
55/// 2. Python objects might reference Rust objects → creates cycles.
56/// 3. Neither side can be garbage collected → memory leak.
57///
58/// By using plain `PyObject` with GIL-based cloning instead of `Arc<PyObject>`, we:
59/// - Avoid circular references between Rust and Python memory management.
60/// - Ensure proper Python reference counting under the GIL.
61/// - Allow both Rust and Python garbage collectors to work correctly.
62///
63/// # Safety
64///
65/// This function properly acquires the Python GIL before performing the clone operation,
66/// ensuring thread-safe access to the Python object and correct reference counting.
67#[must_use]
68pub fn clone_py_object(obj: &PyObject) -> PyObject {
69 Python::with_gil(|py| obj.clone_ref(py))
70}
71
72/// Extend `IntoPyObjectExt` helper trait to unwrap `PyObject` after conversion.
73pub trait IntoPyObjectNautilusExt<'py>: IntoPyObjectExt<'py> {
74 /// Convert `self` into a [`PyObject`] while *panicking* if the conversion fails.
75 ///
76 /// This is a convenience wrapper around [`IntoPyObjectExt::into_py_any`] that avoids the
77 /// cumbersome `Result` handling when we are certain that the conversion cannot fail (for
78 /// instance when we are converting primitives or other types that already implement the
79 /// necessary PyO3 traits).
80 #[inline]
81 fn into_py_any_unwrap(self, py: Python<'py>) -> PyObject {
82 self.into_py_any(py)
83 .expect("Failed to convert type to PyObject")
84 }
85}
86
87impl<'py, T> IntoPyObjectNautilusExt<'py> for T where T: IntoPyObjectExt<'py> {}
88
89/// Gets the type name for the given Python `obj`.
90///
91/// # Errors
92///
93/// Returns a error if accessing the type name fails.
94pub fn get_pytype_name<'py>(obj: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyString>> {
95 obj.get_type().name()
96}
97
98/// Converts any type that implements `Display` to a Python `ValueError`.
99///
100/// # Errors
101///
102/// Returns a Python error with the error string.
103pub fn to_pyvalue_err(e: impl std::fmt::Display) -> PyErr {
104 PyValueError::new_err(e.to_string())
105}
106
107/// Converts any type that implements `Display` to a Python `TypeError`.
108///
109/// # Errors
110///
111/// Returns a Python error with the error string.
112pub fn to_pytype_err(e: impl std::fmt::Display) -> PyErr {
113 PyTypeError::new_err(e.to_string())
114}
115
116/// Converts any type that implements `Display` to a Python `RuntimeError`.
117///
118/// # Errors
119///
120/// Returns a Python error with the error string.
121pub fn to_pyruntime_err(e: impl std::fmt::Display) -> PyErr {
122 PyRuntimeError::new_err(e.to_string())
123}
124
125#[pyfunction]
126#[allow(clippy::needless_pass_by_value)]
127#[allow(unsafe_code)]
128fn is_pycapsule(obj: PyObject) -> bool {
129 unsafe {
130 // PyCapsule_CheckExact checks if the object is exactly a PyCapsule
131 pyo3::ffi::PyCapsule_CheckExact(obj.as_ptr()) != 0
132 }
133}
134
135/// Loaded as `nautilus_pyo3.core`.
136///
137/// # Errors
138///
139/// Returns a `PyErr` if registering any module components fails.
140#[pymodule]
141#[rustfmt::skip]
142pub fn core(_: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
143 m.add(stringify!(NAUTILUS_VERSION), NAUTILUS_VERSION)?;
144 m.add(stringify!(NAUTILUS_USER_AGENT), NAUTILUS_USER_AGENT)?;
145 m.add(stringify!(MILLISECONDS_IN_SECOND), MILLISECONDS_IN_SECOND)?;
146 m.add(stringify!(NANOSECONDS_IN_SECOND), NANOSECONDS_IN_SECOND)?;
147 m.add(stringify!(NANOSECONDS_IN_MILLISECOND), NANOSECONDS_IN_MILLISECOND)?;
148 m.add(stringify!(NANOSECONDS_IN_MICROSECOND), NANOSECONDS_IN_MICROSECOND)?;
149 m.add_class::<UUID4>()?;
150 m.add_function(wrap_pyfunction!(is_pycapsule, m)?)?;
151 m.add_function(wrap_pyfunction!(casing::py_convert_to_snake_case, m)?)?;
152 m.add_function(wrap_pyfunction!(datetime::py_secs_to_nanos, m)?)?;
153 m.add_function(wrap_pyfunction!(datetime::py_secs_to_millis, m)?)?;
154 m.add_function(wrap_pyfunction!(datetime::py_millis_to_nanos, m)?)?;
155 m.add_function(wrap_pyfunction!(datetime::py_micros_to_nanos, m)?)?;
156 m.add_function(wrap_pyfunction!(datetime::py_nanos_to_secs, m)?)?;
157 m.add_function(wrap_pyfunction!(datetime::py_nanos_to_millis, m)?)?;
158 m.add_function(wrap_pyfunction!(datetime::py_nanos_to_micros, m)?)?;
159 m.add_function(wrap_pyfunction!(datetime::py_unix_nanos_to_iso8601, m)?)?;
160 m.add_function(wrap_pyfunction!(datetime::py_last_weekday_nanos, m)?)?;
161 m.add_function(wrap_pyfunction!(datetime::py_is_within_last_24_hours, m)?)?;
162 Ok(())
163}