Skip to main content

nautilus_common/python/
clock.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.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#![warn(clippy::clone_on_ref_ptr)]
17
18use std::{cell::RefCell, rc::Rc};
19
20use jiff::{SignedDuration, Span, Timestamp, Zoned};
21use nautilus_core::{
22    DurationNanos, UnixNanos, datetime::try_datetime_to_unix_nanos, python::to_pyvalue_err,
23};
24use pyo3::prelude::*;
25
26use crate::{
27    clock::{Clock, ClockApi, VirtualClock},
28    live::clock::LiveClock,
29    timer::TimeEventCallback,
30};
31
32/// Unified PyO3 interface over both [`VirtualClock`] and [`LiveClock`].
33///
34/// A `PyClock` instance owns a boxed trait object implementing [`Clock`].  It
35/// delegates method calls to this inner clock, allowing a single Python class
36/// to transparently wrap either implementation and eliminating the large
37/// amount of duplicated glue code previously required.
38///
39/// It intentionally does **not** expose a `__new__` constructor to Python -
40/// clocks should be created from Rust and handed over to Python as needed.
41#[allow(non_camel_case_types)]
42#[pyo3::pyclass(
43    module = "nautilus_trader.common",
44    name = "Clock",
45    unsendable,
46    from_py_object
47)]
48#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
49#[derive(Debug, Clone)]
50pub struct PyClock(Rc<RefCell<dyn Clock>>);
51
52#[pymethods]
53#[pyo3_stub_gen::derive::gen_stub_pymethods]
54impl PyClock {
55    #[staticmethod]
56    #[pyo3(name = "new_test")]
57    fn py_new_test() -> Self {
58        Self(Rc::new(RefCell::new(VirtualClock::default())))
59    }
60
61    /// Returns the current UNIX timestamp in nanoseconds (ns).
62    #[pyo3(name = "timestamp_ns")]
63    fn py_timestamp_ns(&self) -> u64 {
64        self.0.borrow().timestamp_ns().as_u64()
65    }
66
67    /// Returns the current UNIX timestamp in microseconds (μs).
68    #[pyo3(name = "timestamp_us")]
69    fn py_timestamp_us(&self) -> u64 {
70        self.0.borrow().timestamp_us()
71    }
72
73    /// Returns the current UNIX timestamp in milliseconds (ms).
74    #[pyo3(name = "timestamp_ms")]
75    fn py_timestamp_ms(&self) -> u64 {
76        self.0.borrow().timestamp_ms()
77    }
78
79    /// Returns the current UNIX timestamp in seconds.
80    #[pyo3(name = "timestamp")]
81    fn py_timestamp(&self) -> f64 {
82        self.0.borrow().timestamp()
83    }
84
85    /// Returns the current UTC timestamp.
86    #[pyo3(name = "utc_now")]
87    fn py_utc_now(&self) -> Timestamp {
88        self.0.borrow().utc_now()
89    }
90
91    #[pyo3(name = "set_time")]
92    fn py_set_time(&mut self, to_time_ns: u64) -> PyResult<()> {
93        let mut clock = self.0.borrow_mut();
94        let Some(test_clock) = clock.as_any_mut().downcast_mut::<VirtualClock>() else {
95            return Err(to_pyvalue_err(
96                "set_time is only supported by virtual clocks",
97            ));
98        };
99
100        test_clock.set_time(to_time_ns.into());
101        Ok(())
102    }
103
104    /// Returns the names of active timers in the clock.
105    #[pyo3(name = "timer_names")]
106    fn py_timer_names(&self) -> Vec<String> {
107        self.0
108            .borrow()
109            .timer_names()
110            .into_iter()
111            .map(String::from)
112            .collect()
113    }
114
115    /// Returns the count of active timers in the clock.
116    #[pyo3(name = "timer_count")]
117    fn py_timer_count(&self) -> usize {
118        self.0.borrow().timer_count()
119    }
120
121    #[pyo3(name = "register_default_handler")]
122    fn py_register_default_handler(&mut self, callback: Py<PyAny>) {
123        self.0
124            .borrow_mut()
125            .register_default_handler(TimeEventCallback::from(callback));
126    }
127
128    #[pyo3(name = "cancel_default_handler")]
129    fn py_cancel_default_handler(&mut self) {
130        self.0.borrow_mut().cancel_default_handler();
131    }
132
133    #[pyo3(name = "cancel_callbacks")]
134    fn py_cancel_callbacks(&mut self) {
135        self.0.borrow_mut().cancel_callbacks();
136    }
137
138    #[pyo3(
139        name = "set_time_alert",
140        signature = (name, alert_time, callback=None, allow_past=None)
141    )]
142    fn py_set_time_alert(
143        &mut self,
144        name: &str,
145        alert_time: Timestamp,
146        callback: Option<Py<PyAny>>,
147        allow_past: Option<bool>,
148    ) -> PyResult<()> {
149        self.0
150            .borrow_mut()
151            .set_time_alert(
152                name,
153                alert_time,
154                callback.map(TimeEventCallback::from),
155                allow_past,
156            )
157            .map_err(to_pyvalue_err)
158    }
159
160    #[pyo3(
161        name = "set_time_alert_ns",
162        signature = (name, alert_time_ns, callback=None, allow_past=None)
163    )]
164    fn py_set_time_alert_ns(
165        &mut self,
166        name: &str,
167        alert_time_ns: u64,
168        callback: Option<Py<PyAny>>,
169        allow_past: Option<bool>,
170    ) -> PyResult<()> {
171        self.0
172            .borrow_mut()
173            .set_time_alert_ns(
174                name,
175                alert_time_ns.into(),
176                callback.map(TimeEventCallback::from),
177                allow_past,
178            )
179            .map_err(to_pyvalue_err)
180    }
181
182    #[expect(clippy::too_many_arguments)]
183    #[pyo3(
184        name = "set_timer",
185        signature = (name, interval, start_time=None, stop_time=None, callback=None, allow_past=None, fire_immediately=None)
186    )]
187    fn py_set_timer(
188        &mut self,
189        name: &str,
190        interval: SignedDuration,
191        start_time: Option<Timestamp>,
192        stop_time: Option<Timestamp>,
193        callback: Option<Py<PyAny>>,
194        allow_past: Option<bool>,
195        fire_immediately: Option<bool>,
196    ) -> PyResult<()> {
197        if interval <= SignedDuration::ZERO {
198            return Err(to_pyvalue_err("Interval must be positive"));
199        }
200        let interval_ns =
201            DurationNanos::try_from(interval).map_err(|_| to_pyvalue_err("Interval too large"))?;
202
203        let start_time_ns = start_time
204            .map(try_datetime_to_unix_nanos)
205            .transpose()
206            .map_err(to_pyvalue_err)?;
207        let stop_time_ns = stop_time
208            .map(try_datetime_to_unix_nanos)
209            .transpose()
210            .map_err(to_pyvalue_err)?;
211
212        self.0
213            .borrow_mut()
214            .set_timer_ns(
215                name,
216                interval_ns,
217                start_time_ns,
218                stop_time_ns,
219                callback.map(TimeEventCallback::from),
220                allow_past,
221                fire_immediately,
222            )
223            .map_err(to_pyvalue_err)
224    }
225
226    #[expect(clippy::too_many_arguments)]
227    #[pyo3(
228        name = "set_timer_ns",
229        signature = (name, interval_ns, start_time_ns=None, stop_time_ns=None, callback=None, allow_past=None, fire_immediately=None)
230    )]
231    fn py_set_timer_ns(
232        &mut self,
233        name: &str,
234        interval_ns: u64,
235        start_time_ns: Option<u64>,
236        stop_time_ns: Option<u64>,
237        callback: Option<Py<PyAny>>,
238        allow_past: Option<bool>,
239        fire_immediately: Option<bool>,
240    ) -> PyResult<()> {
241        self.0
242            .borrow_mut()
243            .set_timer_ns(
244                name,
245                DurationNanos::new(interval_ns),
246                start_time_ns.map(UnixNanos::from),
247                stop_time_ns.map(UnixNanos::from),
248                callback.map(TimeEventCallback::from),
249                allow_past,
250                fire_immediately,
251            )
252            .map_err(to_pyvalue_err)
253    }
254
255    /// Sets a recurring timer anchored to a timezone-aware start datetime.
256    ///
257    /// Days, weeks, months, and years follow the local calendar; hour and shorter units measure
258    /// elapsed time. At 09:30 in New York, ``"1d"`` events are 23 hours apart when DST starts
259    /// and 25 when it ends; ``"24h"`` always spans 24 hours. ``zoneinfo.ZoneInfo`` follows DST,
260    /// while fixed offsets stay constant.
261    ///
262    /// Recurrence uses the original start, so month-end clamping and DST adjustments do not
263    /// shift later deadlines. Later gaps shift forward by the gap; folds use the first occurrence.
264    ///
265    /// Parameters
266    /// ----------
267    /// name : str
268    ///     The timer name. Replaces any existing timer with the same name on this clock.
269    /// interval : str
270    ///     A positive span string, such as ``"1d"``, ``"24h"``, or ``"P1M"``.
271    /// start_time : datetime.datetime
272    ///     Timezone-aware start at an existing local time. DST gaps at the start are not validated;
273    ///     ``fold`` selects an ambiguous start's occurrence.
274    /// stop_time : datetime.datetime, optional
275    ///     Inclusive, timezone-aware stop after the start, and after now if ``allow_past=False``.
276    ///     If omitted, runs until cancellation or no further deadline is representable.
277    /// callback : Callable[[TimeEvent], None], optional
278    ///     The event handler. If omitted, uses the clock's named or default handler.
279    /// allow_past : bool, optional
280    ///     Whether the first event may be in the past. Defaults to ``True``.
281    /// fire_immediately : bool, optional
282    ///     Include the start as the first event. Defaults to ``False``: one interval after it.
283    ///
284    /// Raises
285    /// ------
286    /// ValueError
287    ///     If the name, span, or timestamps are invalid, no callback is available, or a first
288    ///     event or stop time violates ``allow_past=False``.
289    ///
290    /// Notes
291    /// -----
292    /// Live clocks fire the next pending occurrence immediately if overdue, using its scheduled
293    /// timestamp, then resume at the first deadline after now, skipping intervening missed
294    /// occurrences. Virtual clocks emit every due occurrence during advancement.
295    /// Event timestamps remain UTC UNIX nanoseconds.
296    #[expect(
297        clippy::doc_markdown,
298        reason = "Python API docstrings use NumPy parameter and exception sections"
299    )]
300    #[expect(
301        clippy::too_many_arguments,
302        clippy::needless_pass_by_value,
303        reason = "PyO3 extracts the aware datetime as an owned Zoned"
304    )]
305    #[pyo3(
306        name = "set_timer_zoned",
307        signature = (name, interval, start_time, stop_time=None, callback=None, allow_past=None, fire_immediately=None)
308    )]
309    fn py_set_timer_zoned(
310        &mut self,
311        name: &str,
312        interval: &str,
313        start_time: Zoned,
314        stop_time: Option<Timestamp>,
315        callback: Option<Py<PyAny>>,
316        allow_past: Option<bool>,
317        fire_immediately: Option<bool>,
318    ) -> PyResult<()> {
319        let interval = interval.parse::<Span>().map_err(to_pyvalue_err)?;
320
321        ClockApi::new(self.0.as_ref())
322            .set_timer_zoned(
323                name,
324                interval,
325                &start_time,
326                stop_time,
327                callback.map(TimeEventCallback::from),
328                allow_past,
329                fire_immediately,
330            )
331            .map_err(to_pyvalue_err)
332    }
333
334    #[pyo3(name = "next_time_ns")]
335    fn py_next_time_ns(&self, name: &str) -> Option<u64> {
336        self.0.borrow().next_time_ns(name).map(|t| t.as_u64())
337    }
338
339    #[pyo3(name = "cancel_timer")]
340    fn py_cancel_timer(&mut self, name: &str) {
341        self.0.borrow_mut().cancel_timer(name);
342    }
343
344    #[pyo3(name = "cancel_timers")]
345    fn py_cancel_timers(&mut self) {
346        self.0.borrow_mut().cancel_timers();
347    }
348}
349
350impl PyClock {
351    /// Creates a `PyClock` directly from an `Rc<RefCell<dyn Clock>>`.
352    #[must_use]
353    pub fn from_rc(rc: Rc<RefCell<dyn Clock>>) -> Self {
354        Self(rc)
355    }
356
357    /// Gets the inner `Rc<RefCell<dyn Clock>>` for use in Rust code.
358    #[must_use]
359    pub fn clock_rc(&self) -> Rc<RefCell<dyn Clock>> {
360        Rc::clone(&self.0)
361    }
362
363    /// Creates a clock backed by [`VirtualClock`].
364    #[must_use]
365    pub fn new_test() -> Self {
366        Self(Rc::new(RefCell::new(VirtualClock::default())))
367    }
368
369    /// Creates a clock backed by [`LiveClock`].
370    #[must_use]
371    pub fn new_live() -> Self {
372        Self(Rc::new(RefCell::new(LiveClock::default())))
373    }
374
375    /// Provides access to the inner [`Clock`] trait object.
376    #[must_use]
377    pub fn inner(&self) -> std::cell::Ref<'_, dyn Clock> {
378        self.0.borrow()
379    }
380
381    /// Mutably accesses the underlying [`Clock`].
382    #[must_use]
383    pub fn inner_mut(&mut self) -> std::cell::RefMut<'_, dyn Clock> {
384        self.0.borrow_mut()
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use std::sync::Arc;
391
392    use jiff::{SignedDuration, Timestamp};
393    use nautilus_core::{DurationNanos, UnixNanos, python::IntoPyObjectNautilusExt};
394    use pyo3::{prelude::*, types::PyList};
395    use rstest::*;
396
397    use crate::{
398        clock::{Clock, VirtualClock},
399        python::clock::PyClock,
400        runner::{TimeEventMessage, TimeEventSender, set_time_event_sender},
401        timer::TimeEventCallback,
402    };
403
404    fn ensure_sender() {
405        if crate::runner::try_get_time_event_sender().is_none() {
406            set_time_event_sender(Arc::new(DummySender));
407        }
408    }
409
410    // Dummy TimeEventSender for LiveClock tests
411    #[derive(Debug)]
412    struct DummySender;
413
414    impl TimeEventSender for DummySender {
415        fn send(&self, _message: TimeEventMessage) {}
416    }
417
418    #[fixture]
419    pub fn test_clock() -> VirtualClock {
420        VirtualClock::new()
421    }
422
423    pub(super) fn test_callback() -> TimeEventCallback {
424        Python::initialize();
425        Python::attach(|py| {
426            let py_list = PyList::empty(py);
427            let py_append = Py::from(py_list.getattr("append").unwrap());
428            let py_append = py_append.into_py_any_unwrap(py);
429            TimeEventCallback::from(py_append)
430        })
431    }
432
433    pub(super) fn test_py_callback() -> Py<PyAny> {
434        Python::initialize();
435        Python::attach(|py| {
436            let py_list = PyList::empty(py);
437            let py_append = Py::from(py_list.getattr("append").unwrap());
438            py_append.into_py_any_unwrap(py)
439        })
440    }
441
442    ////////////////////////////////////////////////////////////////////////////////
443    // VirtualClock_Py
444    ////////////////////////////////////////////////////////////////////////////////
445
446    #[rstest]
447    fn test_test_clock_py_set_time_alert() {
448        Python::initialize();
449        Python::attach(|_py| {
450            let mut py_clock = PyClock::new_test();
451            let callback = test_py_callback();
452            py_clock.py_register_default_handler(callback);
453            let dt = Timestamp::now() + SignedDuration::from_secs(1);
454            py_clock
455                .py_set_time_alert("ALERT1", dt, None, None)
456                .expect("set_time_alert failed");
457        });
458    }
459
460    #[rstest]
461    fn test_test_clock_py_set_time() {
462        Python::initialize();
463        Python::attach(|_py| {
464            let mut py_clock = PyClock::new_test();
465
466            py_clock.py_set_time(1_700_000_000_000_000_000).unwrap();
467
468            assert_eq!(py_clock.py_timestamp_ns(), 1_700_000_000_000_000_000);
469        });
470    }
471
472    #[rstest]
473    fn test_test_clock_py_set_timer() {
474        Python::initialize();
475        Python::attach(|_py| {
476            let mut py_clock = PyClock::new_test();
477            let callback = test_py_callback();
478            py_clock.py_register_default_handler(callback);
479            let interval = SignedDuration::from_secs(2);
480            py_clock
481                .py_set_timer("TIMER1", interval, None, None, None, None, None)
482                .expect("set_timer failed");
483        });
484    }
485
486    #[rstest]
487    fn test_test_clock_py_set_timer_rejects_unconvertible_datetime() {
488        Python::initialize();
489        Python::attach(|_py| {
490            let mut py_clock = PyClock::new_test();
491            let callback = test_py_callback();
492            py_clock.py_register_default_handler(callback);
493            let interval = SignedDuration::from_secs(2);
494            let pre_epoch = Timestamp::from_nanosecond(-1).unwrap();
495
496            let err = py_clock
497                .py_set_timer(
498                    "PRE_EPOCH_START",
499                    interval,
500                    Some(pre_epoch),
501                    None,
502                    None,
503                    None,
504                    None,
505                )
506                .expect_err("set_timer should reject a pre-epoch start time");
507            assert!(
508                err.to_string().contains("cannot be negative"),
509                "unexpected error: {err}"
510            );
511
512            let err = py_clock
513                .py_set_timer(
514                    "PRE_EPOCH_STOP",
515                    interval,
516                    None,
517                    Some(pre_epoch),
518                    None,
519                    None,
520                    None,
521                )
522                .expect_err("set_timer should reject a pre-epoch stop time");
523            assert!(
524                err.to_string().contains("cannot be negative"),
525                "unexpected error: {err}"
526            );
527
528            assert_eq!(py_clock.py_timer_count(), 0);
529        });
530    }
531
532    #[rstest]
533    fn test_test_clock_py_set_time_alert_ns() {
534        Python::initialize();
535        Python::attach(|_py| {
536            let mut py_clock = PyClock::new_test();
537            let callback = test_py_callback();
538            py_clock.py_register_default_handler(callback);
539            let ts_ns = (Timestamp::now() + SignedDuration::from_secs(1)).as_nanosecond();
540            let ts_ns = u64::try_from(ts_ns).unwrap();
541            py_clock
542                .py_set_time_alert_ns("ALERT_NS", ts_ns, None, None)
543                .expect("set_time_alert_ns failed");
544        });
545    }
546
547    #[rstest]
548    fn test_test_clock_py_set_timer_ns() {
549        Python::initialize();
550        Python::attach(|_py| {
551            let mut py_clock = PyClock::new_test();
552            let callback = test_py_callback();
553            py_clock.py_register_default_handler(callback);
554            py_clock
555                .py_set_timer_ns("TIMER_NS", 1_000_000, None, None, None, None, None)
556                .expect("set_timer_ns failed");
557        });
558    }
559
560    #[rstest]
561    fn test_test_clock_raw_set_timer_ns(mut test_clock: VirtualClock) {
562        Python::initialize();
563        Python::attach(|_py| {
564            let callback = test_callback();
565            test_clock.register_default_handler(callback);
566
567            let timer_name = "TEST_TIME1";
568            test_clock
569                .set_timer_ns(
570                    timer_name,
571                    DurationNanos::new(10),
572                    None,
573                    None,
574                    None,
575                    None,
576                    None,
577                )
578                .unwrap();
579
580            assert_eq!(test_clock.timer_names(), [timer_name]);
581            assert_eq!(test_clock.timer_count(), 1);
582        });
583    }
584
585    #[rstest]
586    fn test_test_clock_cancel_timer(mut test_clock: VirtualClock) {
587        Python::initialize();
588        Python::attach(|_py| {
589            let callback = test_callback();
590            test_clock.register_default_handler(callback);
591
592            let timer_name = "TEST_TIME1";
593            test_clock
594                .set_timer_ns(
595                    timer_name,
596                    DurationNanos::new(10),
597                    None,
598                    None,
599                    None,
600                    None,
601                    None,
602                )
603                .unwrap();
604            test_clock.cancel_timer(timer_name);
605
606            assert!(test_clock.timer_names().is_empty());
607            assert_eq!(test_clock.timer_count(), 0);
608        });
609    }
610
611    #[rstest]
612    fn test_test_clock_cancel_timers(mut test_clock: VirtualClock) {
613        Python::initialize();
614        Python::attach(|_py| {
615            let callback = test_callback();
616            test_clock.register_default_handler(callback);
617
618            let timer_name = "TEST_TIME1";
619            test_clock
620                .set_timer_ns(
621                    timer_name,
622                    DurationNanos::new(10),
623                    None,
624                    None,
625                    None,
626                    None,
627                    None,
628                )
629                .unwrap();
630            test_clock.cancel_timers();
631
632            assert!(test_clock.timer_names().is_empty());
633            assert_eq!(test_clock.timer_count(), 0);
634        });
635    }
636
637    #[rstest]
638    fn test_test_clock_advance_within_stop_time_py(mut test_clock: VirtualClock) {
639        Python::initialize();
640        Python::attach(|_py| {
641            let callback = test_callback();
642            test_clock.register_default_handler(callback);
643
644            let timer_name = "TEST_TIME1";
645            test_clock
646                .set_timer_ns(
647                    timer_name,
648                    DurationNanos::new(1),
649                    Some(UnixNanos::from(1)),
650                    Some(UnixNanos::from(3)),
651                    None,
652                    None,
653                    None,
654                )
655                .unwrap();
656            test_clock.advance_time(2.into(), true);
657
658            assert_eq!(test_clock.timer_names(), [timer_name]);
659            assert_eq!(test_clock.timer_count(), 1);
660        });
661    }
662
663    #[rstest]
664    fn test_test_clock_advance_time_to_stop_time_with_set_time_true(mut test_clock: VirtualClock) {
665        Python::initialize();
666        Python::attach(|_py| {
667            let callback = test_callback();
668            test_clock.register_default_handler(callback);
669
670            test_clock
671                .set_timer_ns(
672                    "TEST_TIME1",
673                    DurationNanos::new(2),
674                    None,
675                    Some(UnixNanos::from(3)),
676                    None,
677                    None,
678                    None,
679                )
680                .unwrap();
681            test_clock.advance_time(3.into(), true);
682
683            assert_eq!(test_clock.timer_names().len(), 1);
684            assert_eq!(test_clock.timer_count(), 1);
685            assert_eq!(test_clock.get_time_ns(), 3);
686        });
687    }
688
689    #[rstest]
690    fn test_test_clock_advance_time_to_stop_time_with_set_time_false(mut test_clock: VirtualClock) {
691        Python::initialize();
692        Python::attach(|_py| {
693            let callback = test_callback();
694            test_clock.register_default_handler(callback);
695
696            test_clock
697                .set_timer_ns(
698                    "TEST_TIME1",
699                    DurationNanos::new(2),
700                    None,
701                    Some(UnixNanos::from(3)),
702                    None,
703                    None,
704                    None,
705                )
706                .unwrap();
707            test_clock.advance_time(3.into(), false);
708
709            assert_eq!(test_clock.timer_names().len(), 1);
710            assert_eq!(test_clock.timer_count(), 1);
711            assert_eq!(test_clock.get_time_ns(), 0);
712        });
713    }
714
715    ////////////////////////////////////////////////////////////////////////////////
716    // LiveClock_Py
717    ////////////////////////////////////////////////////////////////////////////////
718
719    #[rstest]
720    fn test_live_clock_py_set_time_alert() {
721        ensure_sender();
722
723        Python::initialize();
724        Python::attach(|_py| {
725            let mut py_clock = PyClock::new_live();
726            let callback = test_py_callback();
727            py_clock.py_register_default_handler(callback);
728            let dt = Timestamp::now() + SignedDuration::from_secs(1);
729
730            py_clock
731                .py_set_time_alert("ALERT1", dt, None, None)
732                .expect("live set_time_alert failed");
733        });
734    }
735
736    #[rstest]
737    fn test_live_clock_py_set_time_returns_error() {
738        Python::initialize();
739        Python::attach(|_py| {
740            let mut py_clock = PyClock::new_live();
741
742            let result = py_clock.py_set_time(1_700_000_000_000_000_000);
743
744            assert_eq!(
745                result.unwrap_err().to_string(),
746                "ValueError: set_time is only supported by virtual clocks",
747            );
748        });
749    }
750
751    #[rstest]
752    fn test_live_clock_py_set_timer() {
753        ensure_sender();
754
755        Python::initialize();
756        Python::attach(|_py| {
757            let mut py_clock = PyClock::new_live();
758            let callback = test_py_callback();
759            py_clock.py_register_default_handler(callback);
760            let interval = SignedDuration::from_secs(3);
761
762            py_clock
763                .py_set_timer("TIMER1", interval, None, None, None, None, None)
764                .expect("live set_timer failed");
765        });
766    }
767
768    #[rstest]
769    fn test_live_clock_py_set_time_alert_ns() {
770        ensure_sender();
771
772        Python::initialize();
773        Python::attach(|_py| {
774            let mut py_clock = PyClock::new_live();
775            let callback = test_py_callback();
776            py_clock.py_register_default_handler(callback);
777            let dt_ns = (Timestamp::now() + SignedDuration::from_secs(1)).as_nanosecond();
778            let dt_ns = u64::try_from(dt_ns).unwrap();
779
780            py_clock
781                .py_set_time_alert_ns("ALERT_NS", dt_ns, None, None)
782                .expect("live set_time_alert_ns failed");
783        });
784    }
785
786    #[rstest]
787    fn test_live_clock_py_set_timer_ns() {
788        ensure_sender();
789
790        Python::initialize();
791        Python::attach(|_py| {
792            let mut py_clock = PyClock::new_live();
793            let callback = test_py_callback();
794            py_clock.py_register_default_handler(callback);
795            let interval_ns = 1_000_000_000_u64; // 1 second
796
797            py_clock
798                .py_set_timer_ns("TIMER_NS", interval_ns, None, None, None, None, None)
799                .expect("live set_timer_ns failed");
800        });
801    }
802}