nautilus_core/python/
mod.rs1#![expect(clippy::doc_markdown, reason = "Python docstrings")]
17
18#![allow(
21 deprecated,
22 reason = "pyo3-stub-gen currently relies on PyO3 initialization helpers marked as deprecated"
23)]
24#![expect(
25 clippy::missing_errors_doc,
26 reason = "errors documented on underlying Rust methods"
27)]
28pub mod casing;
35pub mod datetime;
36pub mod enums;
37pub mod params;
38pub mod parsing;
39pub mod serialization;
40pub mod string;
42pub mod uuid;
43pub mod version;
44
45use std::fmt::Display;
46
47use pyo3::{
48 Py,
49 conversion::IntoPyObjectExt,
50 exceptions::{
51 PyException, PyKeyError, PyNotImplementedError, PyRuntimeError, PyTypeError, PyValueError,
52 },
53 prelude::*,
54 types::PyString,
55 wrap_pyfunction,
56};
57use pyo3_stub_gen::derive::gen_stub_pyfunction;
58
59use crate::{
60 UUID4,
61 consts::{NAUTILUS_USER_AGENT, NAUTILUS_VERSION},
62 correctness::CorrectnessError,
63 datetime::{
64 MILLISECONDS_IN_SECOND, NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND,
65 NANOSECONDS_IN_SECOND,
66 },
67};
68
69#[must_use]
84pub fn clone_py_object(obj: &Py<PyAny>) -> Py<PyAny> {
85 Python::attach(|py| obj.clone_ref(py))
86}
87
88pub fn call_python(py: Python, callback: &Py<PyAny>, py_obj: Py<PyAny>) {
90 if let Err(e) = callback.call1(py, (py_obj,)) {
91 log::error!("Error calling Python: {e}");
92 }
93}
94
95pub fn call_python_threadsafe(
101 py: Python,
102 call_soon: &Py<PyAny>,
103 callback: &Py<PyAny>,
104 py_obj: Py<PyAny>,
105) {
106 if let Err(e) = call_soon.call1(py, (callback, py_obj)) {
107 log::error!("Error scheduling Python callback on event loop: {e}");
108 }
109}
110
111pub trait IntoPyObjectNautilusExt<'py>: IntoPyObjectExt<'py> {
113 #[inline]
120 fn into_py_any_unwrap(self, py: Python<'py>) -> Py<PyAny> {
121 self.into_py_any(py)
122 .expect("Failed to convert type to Py<PyAny>")
123 }
124}
125
126impl<'py, T> IntoPyObjectNautilusExt<'py> for T where T: IntoPyObjectExt<'py> {}
127
128pub fn get_pytype_name<'py>(obj: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyString>> {
134 obj.get_type().name()
135}
136
137pub fn to_pyvalue_err(e: impl Display) -> PyErr {
139 PyValueError::new_err(e.to_string())
140}
141
142#[must_use]
144#[allow(
145 clippy::needless_pass_by_value,
146 reason = "Result::map_err passes owned errors to conversion functions"
147)]
148pub fn correctness_error_to_pyvalue_err(e: CorrectnessError) -> PyErr {
149 PyValueError::new_err(e.to_string())
150}
151
152pub fn to_pytype_err(e: impl Display) -> PyErr {
154 PyTypeError::new_err(e.to_string())
155}
156
157pub fn to_pyruntime_err(e: impl Display) -> PyErr {
159 PyRuntimeError::new_err(e.to_string())
160}
161
162pub fn to_pykey_err(e: impl Display) -> PyErr {
164 PyKeyError::new_err(e.to_string())
165}
166
167pub fn to_pyexception(e: impl Display) -> PyErr {
169 PyException::new_err(e.to_string())
170}
171
172pub fn to_pynotimplemented_err(e: impl Display) -> PyErr {
174 PyNotImplementedError::new_err(e.to_string())
175}
176
177#[pyfunction(name = "is_pycapsule")]
188#[gen_stub_pyfunction(module = "nautilus_trader.core")]
189#[expect(
190 clippy::needless_pass_by_value,
191 reason = "Python FFI requires owned types"
192)]
193#[allow(unsafe_code)]
194fn py_is_pycapsule(obj: Py<PyAny>) -> bool {
195 unsafe {
197 pyo3::ffi::PyCapsule_CheckExact(obj.as_ptr()) != 0
199 }
200}
201
202#[pymodule]
208#[rustfmt::skip]
209pub fn core(_: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
210 m.add(stringify!(NAUTILUS_VERSION), NAUTILUS_VERSION)?;
211 m.add(stringify!(NAUTILUS_USER_AGENT), NAUTILUS_USER_AGENT)?;
212 m.add(stringify!(MILLISECONDS_IN_SECOND), MILLISECONDS_IN_SECOND)?;
213 m.add(stringify!(NANOSECONDS_IN_SECOND), NANOSECONDS_IN_SECOND)?;
214 m.add(stringify!(NANOSECONDS_IN_MILLISECOND), NANOSECONDS_IN_MILLISECOND)?;
215 m.add(stringify!(NANOSECONDS_IN_MICROSECOND), NANOSECONDS_IN_MICROSECOND)?;
216 m.add_class::<UUID4>()?;
217 m.add_function(wrap_pyfunction!(py_is_pycapsule, m)?)?;
218 m.add_function(wrap_pyfunction!(casing::py_convert_to_snake_case, m)?)?;
219 m.add_function(wrap_pyfunction!(string::py_mask_api_key, m)?)?;
220 m.add_function(wrap_pyfunction!(datetime::py_secs_to_nanos, m)?)?;
221 m.add_function(wrap_pyfunction!(datetime::py_secs_to_millis, m)?)?;
222 m.add_function(wrap_pyfunction!(datetime::py_millis_to_nanos, m)?)?;
223 m.add_function(wrap_pyfunction!(datetime::py_micros_to_nanos, m)?)?;
224 m.add_function(wrap_pyfunction!(datetime::py_nanos_to_secs, m)?)?;
225 m.add_function(wrap_pyfunction!(datetime::py_nanos_to_millis, m)?)?;
226 m.add_function(wrap_pyfunction!(datetime::py_nanos_to_micros, m)?)?;
227 m.add_function(wrap_pyfunction!(datetime::py_unix_nanos_to_iso8601, m)?)?;
228 m.add_function(wrap_pyfunction!(datetime::py_last_weekday_nanos, m)?)?;
229 m.add_function(wrap_pyfunction!(datetime::py_is_within_last_24_hours, m)?)?;
230 Ok(())
231}
232
233#[cfg(test)]
234mod tests {
235 use std::sync::Once;
236
237 use pyo3::{Python, exceptions::PyValueError};
238 use rstest::rstest;
239
240 use super::*;
241
242 fn ensure_python_initialized() {
243 static INIT: Once = Once::new();
244 INIT.call_once(|| {
245 Python::initialize();
246 });
247 }
248
249 #[rstest]
250 fn test_correctness_error_to_pyvalue_err_preserves_display_text() {
251 ensure_python_initialized();
252
253 let error = CorrectnessError::EmptyString {
254 param: "value".to_string(),
255 };
256
257 Python::attach(|py| {
258 let py_err = correctness_error_to_pyvalue_err(error);
259
260 assert!(py_err.is_instance_of::<PyValueError>(py));
261 assert_eq!(
262 py_err.value(py).to_string(),
263 "invalid string for 'value', was empty"
264 );
265 });
266 }
267}