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