nautilus_core/python/
datetime.rs1use pyo3::prelude::*;
19use pyo3_stub_gen::derive::gen_stub_pyfunction;
20
21use super::to_pyvalue_err;
22use crate::{
23 UnixNanos,
24 datetime::{
25 is_within_last_24_hours, last_weekday_nanos, micros_to_nanos, millis_to_nanos,
26 nanos_to_micros, nanos_to_millis, nanos_to_secs, secs_to_millis, secs_to_nanos,
27 unix_nanos_to_iso8601, unix_nanos_to_iso8601_millis,
28 },
29};
30
31#[pyfunction(name = "secs_to_nanos")]
37#[gen_stub_pyfunction(module = "nautilus_trader.core")]
38pub fn py_secs_to_nanos(secs: f64) -> PyResult<u64> {
39 secs_to_nanos(secs).map_err(to_pyvalue_err)
40}
41
42#[pyfunction(name = "secs_to_millis")]
48#[gen_stub_pyfunction(module = "nautilus_trader.core")]
49pub fn py_secs_to_millis(secs: f64) -> PyResult<u64> {
50 secs_to_millis(secs).map_err(to_pyvalue_err)
51}
52
53#[pyfunction(name = "millis_to_nanos")]
62#[gen_stub_pyfunction(module = "nautilus_trader.core")]
63pub fn py_millis_to_nanos(millis: f64) -> PyResult<u64> {
64 millis_to_nanos(millis).map_err(to_pyvalue_err)
65}
66
67#[pyfunction(name = "micros_to_nanos")]
76#[gen_stub_pyfunction(module = "nautilus_trader.core")]
77pub fn py_micros_to_nanos(micros: f64) -> PyResult<u64> {
78 micros_to_nanos(micros).map_err(to_pyvalue_err)
79}
80
81#[must_use]
86#[pyfunction(name = "nanos_to_secs")]
87#[gen_stub_pyfunction(module = "nautilus_trader.core")]
88pub fn py_nanos_to_secs(nanos: u64) -> f64 {
89 nanos_to_secs(nanos)
90}
91
92#[must_use]
94#[pyfunction(name = "nanos_to_millis")]
95#[gen_stub_pyfunction(module = "nautilus_trader.core")]
96pub const fn py_nanos_to_millis(nanos: u64) -> u64 {
97 nanos_to_millis(nanos)
98}
99
100#[must_use]
102#[pyfunction(name = "nanos_to_micros")]
103#[gen_stub_pyfunction(module = "nautilus_trader.core")]
104pub const fn py_nanos_to_micros(nanos: u64) -> u64 {
105 nanos_to_micros(nanos)
106}
107
108#[pyfunction(
112 name = "unix_nanos_to_iso8601",
113 signature = (timestamp_ns, nanos_precision=Some(true))
114)]
115#[gen_stub_pyfunction(module = "nautilus_trader.core")]
116pub fn py_unix_nanos_to_iso8601(
117 timestamp_ns: u64,
118 nanos_precision: Option<bool>,
119) -> PyResult<String> {
120 let unix_nanos = UnixNanos::from(timestamp_ns);
121 let formatted = if nanos_precision.unwrap_or(true) {
122 unix_nanos_to_iso8601(unix_nanos)
123 } else {
124 unix_nanos_to_iso8601_millis(unix_nanos)
125 };
126
127 Ok(formatted)
128}
129
130#[pyfunction(name = "last_weekday_nanos")]
136#[gen_stub_pyfunction(module = "nautilus_trader.core")]
137pub fn py_last_weekday_nanos(year: i32, month: u32, day: u32) -> PyResult<u64> {
138 Ok(last_weekday_nanos(year, month, day)
139 .map_err(to_pyvalue_err)?
140 .as_u64())
141}
142
143#[pyfunction(name = "is_within_last_24_hours")]
149#[gen_stub_pyfunction(module = "nautilus_trader.core")]
150pub fn py_is_within_last_24_hours(timestamp_ns: u64) -> PyResult<bool> {
151 is_within_last_24_hours(UnixNanos::from(timestamp_ns)).map_err(to_pyvalue_err)
152}
153
154#[cfg(test)]
155mod tests {
156 use jiff::Timestamp;
157 use pyo3::ffi::c_str;
158 use rstest::rstest;
159
160 use super::*;
161
162 #[rstest]
163 #[case(Some(true), 30)]
164 #[case(Some(false), 24)]
165 fn test_py_unix_nanos_to_iso8601_supports_full_range(
166 #[case] nanos_precision: Option<bool>,
167 #[case] expected_len: usize,
168 ) {
169 let result = py_unix_nanos_to_iso8601(u64::MAX, nanos_precision);
170 assert_eq!(result.unwrap().len(), expected_len);
171 }
172
173 #[rstest]
174 fn test_py_unix_nanos_to_iso8601_formats_valid_timestamp() {
175 let output = py_unix_nanos_to_iso8601(0, Some(false)).unwrap();
176 assert_eq!(output, "1970-01-01T00:00:00.000Z");
177 }
178
179 #[rstest]
180 fn test_jiff_timestamp_python_input_accepts_aware_non_utc_datetime() {
181 Python::initialize();
182 Python::attach(|py| {
183 let datetime = py
184 .eval(
185 c_str!(
186 "__import__('datetime').datetime(2024, 1, 15, 13, 30, 45, tzinfo=__import__('datetime').timezone(__import__('datetime').timedelta(hours=-5)))"
187 ),
188 None,
189 None,
190 )
191 .unwrap();
192
193 let timestamp = datetime.extract::<Timestamp>().unwrap();
194 assert_eq!(timestamp, "2024-01-15T18:30:45Z".parse().unwrap());
195 });
196 }
197
198 #[rstest]
199 fn test_jiff_timestamp_python_input_accepts_zoneinfo_datetime() {
200 Python::initialize();
201 Python::attach(|py| {
202 let datetime = py
203 .eval(
204 c_str!(
205 "__import__('datetime').datetime(2024, 1, 15, 13, 30, 45, tzinfo=__import__('zoneinfo').ZoneInfo('America/New_York'))"
206 ),
207 None,
208 None,
209 )
210 .unwrap();
211
212 let timestamp = datetime.extract::<Timestamp>().unwrap();
213 assert_eq!(timestamp, "2024-01-15T18:30:45Z".parse().unwrap());
214 });
215 }
216
217 #[rstest]
218 fn test_jiff_timestamp_python_input_rejects_naive_datetime() {
219 Python::initialize();
220 Python::attach(|py| {
221 let datetime = py
222 .eval(
223 c_str!("__import__('datetime').datetime(2024, 1, 15, 13, 30, 45)"),
224 None,
225 None,
226 )
227 .unwrap();
228
229 assert!(datetime.extract::<Timestamp>().is_err());
230 });
231 }
232}