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