#![expect(clippy::doc_markdown, reason = "Python docstrings")]
#![allow(
deprecated,
reason = "pyo3-stub-gen currently relies on PyO3 initialization helpers marked as deprecated"
)]
#![expect(
clippy::missing_errors_doc,
reason = "errors documented on underlying Rust methods"
)]
#[macro_export]
macro_rules! impl_pyo3_config_getters {
($config:ty { $($field:ident: $field_type:ty),+ $(,)? }) => {
#[pyo3_stub_gen::derive::gen_stub_pymethods]
#[pyo3::pymethods]
#[allow(
clippy::clone_on_copy,
reason = "one macro handles Copy and owned configuration fields"
)]
impl $config {
$(
#[getter]
fn $field(&self) -> $field_type {
self.$field.clone()
}
)+
}
};
}
pub mod casing;
pub mod datetime;
pub mod enums;
pub mod params;
pub mod parsing;
pub mod serialization;
pub mod string;
pub mod uuid;
pub mod version;
use std::{convert::Infallible, fmt::Display};
use pyo3::{
BoundObject, Py,
conversion::IntoPyObjectExt,
exceptions::{
PyException, PyKeyError, PyNotImplementedError, PyRuntimeError, PyTypeError, PyValueError,
},
prelude::*,
types::PyString,
wrap_pyfunction,
};
use pyo3_stub_gen::derive::gen_stub_pyfunction;
use crate::{
UUID4,
consts::{NAUTILUS_USER_AGENT, NAUTILUS_VERSION},
correctness::CorrectnessError,
datetime::{
MILLISECONDS_IN_SECOND, NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND,
NANOSECONDS_IN_SECOND,
},
};
#[must_use]
pub fn clone_py_object(obj: &Py<PyAny>) -> Py<PyAny> {
Python::attach(|py| obj.clone_ref(py))
}
pub fn call_python(py: Python, callback: &Py<PyAny>, py_obj: Py<PyAny>) {
if let Err(e) = callback.call1(py, (py_obj,)) {
log::error!("Error calling Python: {e}");
}
}
pub fn call_python_threadsafe(
py: Python,
call_soon: &Py<PyAny>,
callback: &Py<PyAny>,
py_obj: Py<PyAny>,
) {
if let Err(e) = call_soon.call1(py, (callback, py_obj)) {
log::error!("Error scheduling Python callback on event loop: {e}");
}
}
pub trait IntoPyObjectNautilusExt<'py>: IntoPyObjectExt<'py> {
#[inline]
fn into_py_any_unwrap(self, py: Python<'py>) -> Py<PyAny>
where
Self: IntoPyObject<'py, Error = Infallible>,
{
match self.into_pyobject(py) {
Ok(obj) => obj.into_any().unbind(),
Err(never) => match never {},
}
}
}
impl<'py, T> IntoPyObjectNautilusExt<'py> for T where T: IntoPyObjectExt<'py> {}
pub fn get_pytype_name<'py>(obj: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyString>> {
obj.get_type().name()
}
pub fn to_pyvalue_err(e: impl Display) -> PyErr {
PyValueError::new_err(e.to_string())
}
#[must_use]
#[allow(
clippy::needless_pass_by_value,
reason = "Result::map_err passes owned errors to conversion functions"
)]
pub fn correctness_error_to_pyvalue_err(e: CorrectnessError) -> PyErr {
PyValueError::new_err(e.to_string())
}
pub fn to_pytype_err(e: impl Display) -> PyErr {
PyTypeError::new_err(e.to_string())
}
pub fn to_pyruntime_err(e: impl Display) -> PyErr {
PyRuntimeError::new_err(e.to_string())
}
pub fn to_pykey_err(e: impl Display) -> PyErr {
PyKeyError::new_err(e.to_string())
}
pub fn to_pyexception(e: impl Display) -> PyErr {
PyException::new_err(e.to_string())
}
pub fn to_pynotimplemented_err(e: impl Display) -> PyErr {
PyNotImplementedError::new_err(e.to_string())
}
#[pyfunction(name = "is_pycapsule")]
#[gen_stub_pyfunction(module = "nautilus_trader.core")]
#[expect(
clippy::needless_pass_by_value,
reason = "Python FFI requires owned types"
)]
#[allow(unsafe_code)]
fn py_is_pycapsule(obj: Py<PyAny>) -> bool {
unsafe {
pyo3::ffi::PyCapsule_CheckExact(obj.as_ptr()) != 0
}
}
#[pymodule]
#[rustfmt::skip]
pub fn core(_: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add(stringify!(NAUTILUS_VERSION), NAUTILUS_VERSION)?;
m.add(stringify!(NAUTILUS_USER_AGENT), NAUTILUS_USER_AGENT)?;
m.add(stringify!(MILLISECONDS_IN_SECOND), MILLISECONDS_IN_SECOND)?;
m.add(stringify!(NANOSECONDS_IN_SECOND), NANOSECONDS_IN_SECOND)?;
m.add(stringify!(NANOSECONDS_IN_MILLISECOND), NANOSECONDS_IN_MILLISECOND)?;
m.add(stringify!(NANOSECONDS_IN_MICROSECOND), NANOSECONDS_IN_MICROSECOND)?;
m.add_class::<UUID4>()?;
m.add_function(wrap_pyfunction!(py_is_pycapsule, m)?)?;
m.add_function(wrap_pyfunction!(casing::py_convert_to_snake_case, m)?)?;
m.add_function(wrap_pyfunction!(string::py_mask_api_key, m)?)?;
m.add_function(wrap_pyfunction!(datetime::py_secs_to_nanos, m)?)?;
m.add_function(wrap_pyfunction!(datetime::py_secs_to_millis, m)?)?;
m.add_function(wrap_pyfunction!(datetime::py_millis_to_nanos, m)?)?;
m.add_function(wrap_pyfunction!(datetime::py_micros_to_nanos, m)?)?;
m.add_function(wrap_pyfunction!(datetime::py_nanos_to_secs, m)?)?;
m.add_function(wrap_pyfunction!(datetime::py_nanos_to_millis, m)?)?;
m.add_function(wrap_pyfunction!(datetime::py_nanos_to_micros, m)?)?;
m.add_function(wrap_pyfunction!(datetime::py_unix_nanos_to_iso8601, m)?)?;
m.add_function(wrap_pyfunction!(datetime::py_last_weekday_nanos, m)?)?;
m.add_function(wrap_pyfunction!(datetime::py_is_within_last_24_hours, m)?)?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::sync::Once;
use pyo3::{Python, exceptions::PyValueError};
use rstest::rstest;
use super::*;
fn ensure_python_initialized() {
static INIT: Once = Once::new();
INIT.call_once(|| {
Python::initialize();
});
}
#[rstest]
fn test_correctness_error_to_pyvalue_err_preserves_display_text() {
ensure_python_initialized();
let error = CorrectnessError::EmptyString {
param: "value".to_string(),
};
Python::attach(|py| {
let py_err = correctness_error_to_pyvalue_err(error);
assert!(py_err.is_instance_of::<PyValueError>(py));
assert_eq!(
py_err.value(py).to_string(),
"invalid string for 'value', was empty"
);
});
}
}