nautilus_core/python/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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//! Python bindings from [PyO3](https://pyo3.rs).
17
18pub mod casing;
19pub mod datetime;
20pub mod enums;
21pub mod serialization;
22pub mod uuid;
23pub mod version;
24
25use pyo3::{
26    conversion::IntoPyObjectExt,
27    exceptions::{PyRuntimeError, PyTypeError, PyValueError},
28    prelude::*,
29    types::PyString,
30    wrap_pyfunction,
31};
32
33use crate::{
34    UUID4,
35    consts::{NAUTILUS_USER_AGENT, NAUTILUS_VERSION},
36    datetime::{
37        MILLISECONDS_IN_SECOND, NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND,
38        NANOSECONDS_IN_SECOND,
39    },
40};
41
42/// Extend `IntoPyObjectExt` helper trait to unwrap `PyObject` after conversion.
43pub trait IntoPyObjectPoseiExt<'py>: IntoPyObjectExt<'py> {
44    #[inline]
45    fn into_py_any_unwrap(self, py: Python<'py>) -> PyObject {
46        self.into_py_any(py)
47            .expect("Failed to convert type to PyObject")
48    }
49}
50
51impl<'py, T> IntoPyObjectPoseiExt<'py> for T where T: IntoPyObjectExt<'py> {}
52
53/// Gets the type name for the given Python `obj`.
54///
55/// # Errors
56///
57/// Returns a error if accessing the type name fails.
58pub fn get_pytype_name<'py>(obj: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyString>> {
59    obj.get_type().name()
60}
61
62/// Converts any type that implements `Display` to a Python `ValueError`.
63///
64/// # Errors
65///
66/// Returns a Python error with the error string.
67pub fn to_pyvalue_err(e: impl std::fmt::Display) -> PyErr {
68    PyValueError::new_err(e.to_string())
69}
70
71/// Converts any type that implements `Display` to a Python `TypeError`.
72///
73/// # Errors
74///
75/// Returns a Python error with the error string.
76pub fn to_pytype_err(e: impl std::fmt::Display) -> PyErr {
77    PyTypeError::new_err(e.to_string())
78}
79
80/// Converts any type that implements `Display` to a Python `RuntimeError`.
81///
82/// # Errors
83///
84/// Returns a Python error with the error string.
85pub fn to_pyruntime_err(e: impl std::fmt::Display) -> PyErr {
86    PyRuntimeError::new_err(e.to_string())
87}
88
89#[pyfunction]
90#[allow(clippy::needless_pass_by_value)]
91#[allow(unsafe_code)]
92fn is_pycapsule(obj: PyObject) -> bool {
93    unsafe {
94        // PyCapsule_CheckExact checks if the object is exactly a PyCapsule
95        pyo3::ffi::PyCapsule_CheckExact(obj.as_ptr()) != 0
96    }
97}
98
99/// Loaded as `nautilus_pyo3.core`
100///
101/// # Errors
102///
103/// Returns a `PyErr` if registering any module components fails.
104#[pymodule]
105#[rustfmt::skip]
106pub fn core(_: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
107    m.add(stringify!(NAUTILUS_VERSION), NAUTILUS_VERSION)?;
108    m.add(stringify!(NAUTILUS_USER_AGENT), NAUTILUS_USER_AGENT)?;
109    m.add(stringify!(MILLISECONDS_IN_SECOND), MILLISECONDS_IN_SECOND)?;
110    m.add(stringify!(NANOSECONDS_IN_SECOND), NANOSECONDS_IN_SECOND)?;
111    m.add(stringify!(NANOSECONDS_IN_MILLISECOND), NANOSECONDS_IN_MILLISECOND)?;
112    m.add(stringify!(NANOSECONDS_IN_MICROSECOND), NANOSECONDS_IN_MICROSECOND)?;
113    m.add_class::<UUID4>()?;
114    m.add_function(wrap_pyfunction!(is_pycapsule, m)?)?;
115    m.add_function(wrap_pyfunction!(casing::py_convert_to_snake_case, m)?)?;
116    m.add_function(wrap_pyfunction!(datetime::py_secs_to_nanos, m)?)?;
117    m.add_function(wrap_pyfunction!(datetime::py_secs_to_millis, m)?)?;
118    m.add_function(wrap_pyfunction!(datetime::py_millis_to_nanos, m)?)?;
119    m.add_function(wrap_pyfunction!(datetime::py_micros_to_nanos, m)?)?;
120    m.add_function(wrap_pyfunction!(datetime::py_nanos_to_secs, m)?)?;
121    m.add_function(wrap_pyfunction!(datetime::py_nanos_to_millis, m)?)?;
122    m.add_function(wrap_pyfunction!(datetime::py_nanos_to_micros, m)?)?;
123    m.add_function(wrap_pyfunction!(datetime::py_unix_nanos_to_iso8601, m)?)?;
124    m.add_function(wrap_pyfunction!(datetime::py_last_weekday_nanos, m)?)?;
125    m.add_function(wrap_pyfunction!(datetime::py_is_within_last_24_hours, m)?)?;
126    Ok(())
127}