nautilus_core/python/
mod.rs1#![allow(
19 deprecated,
20 reason = "pyo3-stub-gen currently relies on PyO3 initialization functions marked as deprecated"
21)]
22#![expect(
23 clippy::missing_errors_doc,
24 reason = "errors documented on underlying Rust methods"
25)]
26#[macro_export]
34macro_rules! impl_pyo3_config_getters {
35 ($config:ty { $($field:ident: $field_type:ty),+ $(,)? }) => {
36 #[pyo3_stub_gen::derive::gen_stub_pymethods]
37 #[pyo3::pymethods]
38 #[allow(
39 clippy::clone_on_copy,
40 reason = "one macro handles Copy and owned configuration fields"
41 )]
42 impl $config {
43 $(
44 #[getter]
45 fn $field(&self) -> $field_type {
46 self.$field.clone()
47 }
48 )+
49 }
50 };
51}
52
53pub mod casing;
54pub mod datetime;
55pub mod enums;
56pub mod params;
57pub mod parsing;
58pub mod serialization;
59pub mod string;
61pub mod uuid;
62pub mod version;
63
64use std::{convert::Infallible, fmt::Display};
65
66use pyo3::{
67 BoundObject, Py,
68 conversion::IntoPyObjectExt,
69 exceptions::{
70 PyException, PyKeyError, PyNotImplementedError, PyRuntimeError, PyTypeError, PyValueError,
71 },
72 prelude::*,
73 types::{PyString, PyWeakrefMethods, PyWeakrefReference},
74 wrap_pyfunction,
75};
76
77use crate::{
78 UUID4,
79 consts::{NAUTILUS_USER_AGENT, NAUTILUS_VERSION},
80 correctness::CorrectnessError,
81 datetime::{
82 MILLISECONDS_IN_SECOND, NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND,
83 NANOSECONDS_IN_SECOND,
84 },
85};
86
87#[must_use]
96pub fn clone_py_object(obj: &Py<PyAny>) -> Py<PyAny> {
97 Python::attach(|py| obj.clone_ref(py))
98}
99
100pub fn upgrade_py_weakref(
111 py_self: Option<&Py<PyWeakrefReference>>,
112 owner: &dyn Display,
113) -> PyResult<Option<Py<PyAny>>> {
114 let Some(py_self) = py_self else {
115 return Ok(None);
116 };
117
118 Python::attach(|py| {
119 py_self
120 .bind(py)
121 .upgrade()
122 .map(|wrapper| Some(wrapper.unbind()))
123 .ok_or_else(|| {
124 to_pyruntime_err(format!("Python wrapper for {owner} has been collected"))
125 })
126 })
127}
128
129pub fn call_python(py: Python, callback: &Py<PyAny>, py_obj: Py<PyAny>) {
131 if let Err(e) = callback.call1(py, (py_obj,)) {
132 log::error!("Error calling Python: {e}");
133 }
134}
135
136pub fn call_python_threadsafe(
142 py: Python,
143 call_soon: &Py<PyAny>,
144 callback: &Py<PyAny>,
145 py_obj: Py<PyAny>,
146) {
147 if let Err(e) = call_soon.call1(py, (callback, py_obj)) {
148 log::error!("Error scheduling Python callback on event loop: {e}");
149 }
150}
151
152pub trait IntoPyObjectNautilusExt<'py>: IntoPyObjectExt<'py> {
154 #[inline]
156 fn into_py_any_unwrap(self, py: Python<'py>) -> Py<PyAny>
157 where
158 Self: IntoPyObject<'py, Error = Infallible>,
159 {
160 match self.into_pyobject(py) {
161 Ok(obj) => obj.into_any().unbind(),
162 Err(never) => match never {},
163 }
164 }
165}
166
167impl<'py, T> IntoPyObjectNautilusExt<'py> for T where T: IntoPyObjectExt<'py> {}
168
169pub fn get_pytype_name<'py>(obj: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyString>> {
175 obj.get_type().name()
176}
177
178pub fn to_pyvalue_err(e: impl Display) -> PyErr {
180 PyValueError::new_err(e.to_string())
181}
182
183#[must_use]
185#[allow(
186 clippy::needless_pass_by_value,
187 reason = "Result::map_err passes owned errors to conversion functions"
188)]
189pub fn correctness_error_to_pyvalue_err(e: CorrectnessError) -> PyErr {
190 PyValueError::new_err(e.to_string())
191}
192
193pub fn to_pytype_err(e: impl Display) -> PyErr {
195 PyTypeError::new_err(e.to_string())
196}
197
198pub fn to_pyruntime_err(e: impl Display) -> PyErr {
200 PyRuntimeError::new_err(e.to_string())
201}
202
203pub fn to_pykey_err(e: impl Display) -> PyErr {
205 PyKeyError::new_err(e.to_string())
206}
207
208pub fn to_pyexception(e: impl Display) -> PyErr {
210 PyException::new_err(e.to_string())
211}
212
213pub fn to_pynotimplemented_err(e: impl Display) -> PyErr {
215 PyNotImplementedError::new_err(e.to_string())
216}
217
218#[pymodule]
224#[rustfmt::skip]
225pub fn core(_: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
226 m.add(stringify!(NAUTILUS_VERSION), NAUTILUS_VERSION)?;
227 m.add(stringify!(NAUTILUS_USER_AGENT), NAUTILUS_USER_AGENT)?;
228 m.add(stringify!(MILLISECONDS_IN_SECOND), MILLISECONDS_IN_SECOND)?;
229 m.add(stringify!(NANOSECONDS_IN_SECOND), NANOSECONDS_IN_SECOND)?;
230 m.add(stringify!(NANOSECONDS_IN_MILLISECOND), NANOSECONDS_IN_MILLISECOND)?;
231 m.add(stringify!(NANOSECONDS_IN_MICROSECOND), NANOSECONDS_IN_MICROSECOND)?;
232 m.add_class::<UUID4>()?;
233 m.add_function(wrap_pyfunction!(casing::py_convert_to_snake_case, m)?)?;
234 m.add_function(wrap_pyfunction!(string::py_mask_api_key, m)?)?;
235 m.add_function(wrap_pyfunction!(datetime::py_secs_to_nanos, m)?)?;
236 m.add_function(wrap_pyfunction!(datetime::py_secs_to_millis, m)?)?;
237 m.add_function(wrap_pyfunction!(datetime::py_millis_to_nanos, m)?)?;
238 m.add_function(wrap_pyfunction!(datetime::py_micros_to_nanos, m)?)?;
239 m.add_function(wrap_pyfunction!(datetime::py_nanos_to_secs, m)?)?;
240 m.add_function(wrap_pyfunction!(datetime::py_nanos_to_millis, m)?)?;
241 m.add_function(wrap_pyfunction!(datetime::py_nanos_to_micros, m)?)?;
242 m.add_function(wrap_pyfunction!(datetime::py_unix_nanos_to_iso8601, m)?)?;
243 m.add_function(wrap_pyfunction!(datetime::py_last_weekday_nanos, m)?)?;
244 m.add_function(wrap_pyfunction!(datetime::py_is_within_last_24_hours, m)?)?;
245 Ok(())
246}
247
248#[cfg(test)]
249mod tests {
250 use std::sync::Once;
251
252 use pyo3::{Python, exceptions::PyValueError};
253 use rstest::rstest;
254
255 use super::*;
256
257 fn ensure_python_initialized() {
258 static INIT: Once = Once::new();
259 INIT.call_once(|| {
260 Python::initialize();
261 });
262 }
263
264 #[rstest]
265 fn test_correctness_error_to_pyvalue_err_preserves_display_text() {
266 ensure_python_initialized();
267
268 let error = CorrectnessError::EmptyString {
269 param: "value".to_string(),
270 };
271
272 Python::attach(|py| {
273 let py_err = correctness_error_to_pyvalue_err(error);
274
275 assert!(py_err.is_instance_of::<PyValueError>(py));
276 assert_eq!(
277 py_err.value(py).to_string(),
278 "invalid string for 'value', was empty"
279 );
280 });
281 }
282}