nautilus_core/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#![allow(clippy::doc_markdown, reason = "Python docstrings")]
17
18//! Python bindings and interoperability built using [`PyO3`](https://pyo3.rs).
19
20#![allow(
21 deprecated,
22 reason = "pyo3-stub-gen currently relies on PyO3 initialization helpers marked as deprecated"
23)]
24//!
25//! This sub-module groups together the Rust code that is *only* required when compiling the
26//! `python` feature flag. It provides thin adapters so that NautilusTrader functionality can be
27//! consumed from the `nautilus_trader` Python package without sacrificing type-safety or
28//! performance.
29
30pub mod casing;
31pub mod datetime;
32pub mod enums;
33pub mod parsing;
34pub mod serialization;
35/// String manipulation utilities for Python.
36pub mod string;
37pub mod uuid;
38pub mod version;
39
40use std::fmt::Display;
41
42use pyo3::{
43 Py,
44 conversion::IntoPyObjectExt,
45 exceptions::{
46 PyException, PyKeyError, PyNotImplementedError, PyRuntimeError, PyTypeError, PyValueError,
47 },
48 prelude::*,
49 types::PyString,
50 wrap_pyfunction,
51};
52use pyo3_stub_gen::derive::gen_stub_pyfunction;
53
54use crate::{
55 UUID4,
56 consts::{NAUTILUS_USER_AGENT, NAUTILUS_VERSION},
57 datetime::{
58 MILLISECONDS_IN_SECOND, NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND,
59 NANOSECONDS_IN_SECOND,
60 },
61};
62
63/// Safely clones a Python object by acquiring the GIL and properly managing reference counts.
64///
65/// This function exists to break reference cycles between Rust and Python that can occur
66/// when using `Arc<Py<PyAny>>` in callback-holding structs. The original design wrapped
67/// Python callbacks in `Arc` for thread-safe sharing, but this created circular references:
68///
69/// 1. Rust `Arc` holds Python objects → increases Python reference count.
70/// 2. Python objects might reference Rust objects → creates cycles.
71/// 3. Neither side can be garbage collected → memory leak.
72///
73/// By using plain `Py<PyAny>` with GIL-based cloning instead of `Arc<Py<PyAny>>`, we:
74/// - Avoid circular references between Rust and Python memory management.
75/// - Ensure proper Python reference counting under the GIL.
76/// - Allow both Rust and Python garbage collectors to work correctly.
77///
78/// # Safety
79///
80/// This function properly acquires the Python GIL before performing the clone operation,
81/// ensuring thread-safe access to the Python object and correct reference counting.
82#[must_use]
83pub fn clone_py_object(obj: &Py<PyAny>) -> Py<PyAny> {
84 Python::attach(|py| obj.clone_ref(py))
85}
86
87/// Calls a Python callback with a single argument, logging any errors.
88pub fn call_python(py: Python, callback: &Py<PyAny>, py_obj: Py<PyAny>) {
89 if let Err(e) = callback.call1(py, (py_obj,)) {
90 log::error!("Error calling Python: {e}");
91 }
92}
93
94/// Extend `IntoPyObjectExt` helper trait to unwrap `Py<PyAny>` after conversion.
95pub trait IntoPyObjectNautilusExt<'py>: IntoPyObjectExt<'py> {
96 /// Convert `self` into a [`Py<PyAny>`] while *panicking* if the conversion fails.
97 ///
98 /// This is a convenience wrapper around [`IntoPyObjectExt::into_py_any`] that avoids the
99 /// cumbersome `Result` handling when we are certain that the conversion cannot fail (for
100 /// instance when we are converting primitives or other types that already implement the
101 /// necessary PyO3 traits).
102 #[inline]
103 fn into_py_any_unwrap(self, py: Python<'py>) -> Py<PyAny> {
104 self.into_py_any(py)
105 .expect("Failed to convert type to Py<PyAny>")
106 }
107}
108
109impl<'py, T> IntoPyObjectNautilusExt<'py> for T where T: IntoPyObjectExt<'py> {}
110
111/// Gets the type name for the given Python `obj`.
112///
113/// # Errors
114///
115/// Returns a error if accessing the type name fails.
116pub fn get_pytype_name<'py>(obj: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyString>> {
117 obj.get_type().name()
118}
119
120/// Converts any type that implements `Display` to a Python `ValueError`.
121///
122/// # Errors
123///
124/// Returns a Python error with the error string.
125pub fn to_pyvalue_err(e: impl Display) -> PyErr {
126 PyValueError::new_err(e.to_string())
127}
128
129/// Converts any type that implements `Display` to a Python `TypeError`.
130///
131/// # Errors
132///
133/// Returns a Python error with the error string.
134pub fn to_pytype_err(e: impl Display) -> PyErr {
135 PyTypeError::new_err(e.to_string())
136}
137
138/// Converts any type that implements `Display` to a Python `RuntimeError`.
139///
140/// # Errors
141///
142/// Returns a Python error with the error string.
143pub fn to_pyruntime_err(e: impl Display) -> PyErr {
144 PyRuntimeError::new_err(e.to_string())
145}
146
147/// Converts any type that implements `Display` to a Python `KeyError`.
148///
149/// # Errors
150///
151/// Returns a Python error with the error string.
152pub fn to_pykey_err(e: impl Display) -> PyErr {
153 PyKeyError::new_err(e.to_string())
154}
155
156/// Converts any type that implements `Display` to a Python `Exception`.
157///
158/// # Errors
159///
160/// Returns a Python error with the error string.
161pub fn to_pyexception(e: impl Display) -> PyErr {
162 PyException::new_err(e.to_string())
163}
164
165/// Converts any type that implements `Display` to a Python `NotImplementedError`.
166///
167/// # Errors
168///
169/// Returns a Python error with the error string.
170pub fn to_pynotimplemented_err(e: impl Display) -> PyErr {
171 PyNotImplementedError::new_err(e.to_string())
172}
173
174/// Return a value indicating whether the `obj` is a `PyCapsule`.
175///
176/// Parameters
177/// ----------
178/// obj : Any
179/// The object to check.
180///
181/// Returns
182/// -------
183/// bool
184#[gen_stub_pyfunction(module = "nautilus_trader.core")]
185#[pyfunction(name = "is_pycapsule")]
186#[allow(
187 clippy::needless_pass_by_value,
188 reason = "Python FFI requires owned types"
189)]
190#[allow(unsafe_code)]
191fn py_is_pycapsule(obj: Py<PyAny>) -> bool {
192 // SAFETY: obj.as_ptr() returns a valid Python object pointer
193 unsafe {
194 // PyCapsule_CheckExact checks if the object is exactly a PyCapsule
195 pyo3::ffi::PyCapsule_CheckExact(obj.as_ptr()) != 0
196 }
197}
198
199/// Loaded as `nautilus_pyo3.core`.
200///
201/// # Errors
202///
203/// Returns a `PyErr` if registering any module components fails.
204#[pymodule]
205#[rustfmt::skip]
206pub fn core(_: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
207 m.add(stringify!(NAUTILUS_VERSION), NAUTILUS_VERSION)?;
208 m.add(stringify!(NAUTILUS_USER_AGENT), NAUTILUS_USER_AGENT)?;
209 m.add(stringify!(MILLISECONDS_IN_SECOND), MILLISECONDS_IN_SECOND)?;
210 m.add(stringify!(NANOSECONDS_IN_SECOND), NANOSECONDS_IN_SECOND)?;
211 m.add(stringify!(NANOSECONDS_IN_MILLISECOND), NANOSECONDS_IN_MILLISECOND)?;
212 m.add(stringify!(NANOSECONDS_IN_MICROSECOND), NANOSECONDS_IN_MICROSECOND)?;
213 m.add_class::<UUID4>()?;
214 m.add_function(wrap_pyfunction!(py_is_pycapsule, m)?)?;
215 m.add_function(wrap_pyfunction!(casing::py_convert_to_snake_case, m)?)?;
216 m.add_function(wrap_pyfunction!(string::py_mask_api_key, m)?)?;
217 m.add_function(wrap_pyfunction!(datetime::py_secs_to_nanos, m)?)?;
218 m.add_function(wrap_pyfunction!(datetime::py_secs_to_millis, m)?)?;
219 m.add_function(wrap_pyfunction!(datetime::py_millis_to_nanos, m)?)?;
220 m.add_function(wrap_pyfunction!(datetime::py_micros_to_nanos, m)?)?;
221 m.add_function(wrap_pyfunction!(datetime::py_nanos_to_secs, m)?)?;
222 m.add_function(wrap_pyfunction!(datetime::py_nanos_to_millis, m)?)?;
223 m.add_function(wrap_pyfunction!(datetime::py_nanos_to_micros, m)?)?;
224 m.add_function(wrap_pyfunction!(datetime::py_unix_nanos_to_iso8601, m)?)?;
225 m.add_function(wrap_pyfunction!(datetime::py_last_weekday_nanos, m)?)?;
226 m.add_function(wrap_pyfunction!(datetime::py_is_within_last_24_hours, m)?)?;
227 Ok(())
228}