Skip to main content

pyo3/conversions/
time.rs

1#![cfg(feature = "time")]
2
3//! Conversions to and from [time](https://docs.rs/time/)’s `Date`,
4//! `Duration`, `OffsetDateTime`, `PrimitiveDateTime`, `Time`, `UtcDateTime` and `UtcOffset`.
5//!
6//! # Setup
7//!
8//! To use this feature, add this to your **`Cargo.toml`**:
9//!
10//! ```toml
11//! [dependencies]
12//! time = "0.3"
13#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"),  "\", features = [\"time\"] }")]
14//! ```
15//!
16//! Note that you must use compatible versions of time and PyO3.
17//! The required time version may vary based on the version of PyO3.
18//!
19//! ```rust
20//! use time::{Duration, OffsetDateTime, PrimitiveDateTime, Date, Time, Month};
21//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods};
22//!
23//! fn main() -> PyResult<()> {
24//!     Python::initialize();
25//!     Python::attach(|py| {
26//!         // Create a fixed date and time (2022-01-01 12:00:00 UTC)
27//!         let date = Date::from_calendar_date(2022, Month::January, 1).unwrap();
28//!         let time = Time::from_hms(12, 0, 0).unwrap();
29//!         let primitive_dt = PrimitiveDateTime::new(date, time);
30//!
31//!         // Convert to OffsetDateTime with UTC offset
32//!         let datetime = primitive_dt.assume_utc();
33//!
34//!         // Create a duration of 1 hour
35//!         let duration = Duration::hours(1);
36//!
37//!         // Convert to Python objects
38//!         let py_datetime = datetime.into_pyobject(py)?;
39//!         let py_timedelta = duration.into_pyobject(py)?;
40//!
41//!         // Add the duration to the datetime in Python
42//!         let py_result = py_datetime.add(py_timedelta)?;
43//!
44//!         // Convert the result back to Rust
45//!         let result: OffsetDateTime = py_result.extract()?;
46//!         assert_eq!(result.hour(), 13);
47//!
48//!         Ok(())
49//!     })
50//! }
51//! ```
52
53use crate::exceptions::{PyOverflowError, PyTypeError, PyValueError};
54#[cfg(feature = "experimental-inspect")]
55use crate::inspect::PyStaticExpr;
56#[cfg(Py_LIMITED_API)]
57use crate::intern;
58#[cfg(not(Py_LIMITED_API))]
59use crate::types::datetime::{PyDateAccess, PyDeltaAccess};
60use crate::types::{PyAnyMethods, PyDate, PyDateTime, PyDelta, PyNone, PyTime, PyTzInfo};
61#[cfg(not(Py_LIMITED_API))]
62use crate::types::{PyTimeAccess, PyTzInfoAccess};
63#[cfg(feature = "experimental-inspect")]
64use crate::{type_hint_identifier, PyTypeInfo};
65use crate::{Borrowed, Bound, FromPyObject, IntoPyObject, PyAny, PyErr, PyResult, Python};
66use time::{
67    Date, Duration, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcDateTime, UtcOffset,
68};
69
70const SECONDS_PER_DAY: i64 = 86_400;
71
72// Macro for reference implementation
73macro_rules! impl_into_py_for_ref {
74    ($type:ty, $target:ty) => {
75        impl<'py> IntoPyObject<'py> for &$type {
76            type Target = $target;
77            type Output = Bound<'py, Self::Target>;
78            type Error = PyErr;
79
80            #[cfg(feature = "experimental-inspect")]
81            const OUTPUT_TYPE: PyStaticExpr = <$type>::OUTPUT_TYPE;
82
83            #[inline]
84            fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
85                (*self).into_pyobject(py)
86            }
87        }
88    };
89}
90
91// Macro for month conversion
92macro_rules! month_from_number {
93    ($month:expr) => {
94        match $month {
95            1 => Month::January,
96            2 => Month::February,
97            3 => Month::March,
98            4 => Month::April,
99            5 => Month::May,
100            6 => Month::June,
101            7 => Month::July,
102            8 => Month::August,
103            9 => Month::September,
104            10 => Month::October,
105            11 => Month::November,
106            12 => Month::December,
107            _ => return Err(PyValueError::new_err("invalid month value")),
108        }
109    };
110}
111
112fn extract_date_time(dt: &Bound<'_, PyAny>) -> PyResult<(Date, Time)> {
113    #[cfg(not(Py_LIMITED_API))]
114    {
115        let dt = dt.cast::<PyDateTime>()?;
116        let date = Date::from_calendar_date(
117            dt.get_year(),
118            month_from_number!(dt.get_month()),
119            dt.get_day(),
120        )
121        .map_err(|_| PyValueError::new_err("invalid or out-of-range date"))?;
122
123        let time = Time::from_hms_micro(
124            dt.get_hour(),
125            dt.get_minute(),
126            dt.get_second(),
127            dt.get_microsecond(),
128        )
129        .map_err(|_| PyValueError::new_err("invalid or out-of-range time"))?;
130        Ok((date, time))
131    }
132
133    #[cfg(Py_LIMITED_API)]
134    {
135        let date = Date::from_calendar_date(
136            dt.getattr(intern!(dt.py(), "year"))?.extract()?,
137            month_from_number!(dt.getattr(intern!(dt.py(), "month"))?.extract::<u8>()?),
138            dt.getattr(intern!(dt.py(), "day"))?.extract()?,
139        )
140        .map_err(|_| PyValueError::new_err("invalid or out-of-range date"))?;
141
142        let time = Time::from_hms_micro(
143            dt.getattr(intern!(dt.py(), "hour"))?.extract()?,
144            dt.getattr(intern!(dt.py(), "minute"))?.extract()?,
145            dt.getattr(intern!(dt.py(), "second"))?.extract()?,
146            dt.getattr(intern!(dt.py(), "microsecond"))?.extract()?,
147        )
148        .map_err(|_| PyValueError::new_err("invalid or out-of-range time"))?;
149
150        Ok((date, time))
151    }
152}
153
154impl<'py> IntoPyObject<'py> for Duration {
155    type Target = PyDelta;
156    type Output = Bound<'py, Self::Target>;
157    type Error = PyErr;
158
159    #[cfg(feature = "experimental-inspect")]
160    const OUTPUT_TYPE: PyStaticExpr = PyDelta::TYPE_HINT;
161
162    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
163        let total_seconds = self.whole_seconds();
164        let micro_seconds = self.subsec_microseconds();
165
166        // For negative durations, Python expects days to be negative and
167        // seconds/microseconds to be positive or zero
168        let (days, seconds) = if total_seconds < 0 && total_seconds % SECONDS_PER_DAY != 0 {
169            // For negative values, we need to round down (toward more negative)
170            // e.g., -10 seconds should be -1 days + 86390 seconds
171            let days = total_seconds.div_euclid(SECONDS_PER_DAY);
172            let seconds = total_seconds.rem_euclid(SECONDS_PER_DAY);
173            (days, seconds)
174        } else {
175            // For positive or exact negative days, use normal division
176            (
177                total_seconds / SECONDS_PER_DAY,
178                total_seconds % SECONDS_PER_DAY,
179            )
180        };
181        let days = days
182            .try_into()
183            .map_err(|_| PyOverflowError::new_err("duration out of range for Python timedelta"))?;
184
185        PyDelta::new(
186            py,
187            days,
188            seconds.try_into().expect("seconds overflow"),
189            micro_seconds,
190            true,
191        )
192    }
193}
194
195impl FromPyObject<'_, '_> for Duration {
196    type Error = PyErr;
197
198    #[cfg(feature = "experimental-inspect")]
199    const INPUT_TYPE: PyStaticExpr = PyDelta::TYPE_HINT;
200
201    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
202        #[cfg(not(Py_LIMITED_API))]
203        let (days, seconds, microseconds) = {
204            let delta = ob.cast::<PyDelta>()?;
205            (
206                delta.get_days().into(),
207                delta.get_seconds().into(),
208                delta.get_microseconds().into(),
209            )
210        };
211
212        #[cfg(Py_LIMITED_API)]
213        let (days, seconds, microseconds) = {
214            (
215                ob.getattr(intern!(ob.py(), "days"))?.extract()?,
216                ob.getattr(intern!(ob.py(), "seconds"))?.extract()?,
217                ob.getattr(intern!(ob.py(), "microseconds"))?.extract()?,
218            )
219        };
220
221        Ok(
222            Duration::days(days)
223                + Duration::seconds(seconds)
224                + Duration::microseconds(microseconds),
225        )
226    }
227}
228
229impl<'py> IntoPyObject<'py> for Date {
230    type Target = PyDate;
231    type Output = Bound<'py, Self::Target>;
232    type Error = PyErr;
233
234    #[cfg(feature = "experimental-inspect")]
235    const OUTPUT_TYPE: PyStaticExpr = PyDate::TYPE_HINT;
236
237    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
238        let year = self.year();
239        let month = self.month() as u8;
240        let day = self.day();
241
242        PyDate::new(py, year, month, day)
243    }
244}
245
246impl FromPyObject<'_, '_> for Date {
247    type Error = PyErr;
248
249    #[cfg(feature = "experimental-inspect")]
250    const INPUT_TYPE: PyStaticExpr = PyDate::TYPE_HINT;
251
252    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
253        let (year, month, day) = {
254            #[cfg(not(Py_LIMITED_API))]
255            {
256                let date = ob.cast::<PyDate>()?;
257                (date.get_year(), date.get_month(), date.get_day())
258            }
259
260            #[cfg(Py_LIMITED_API)]
261            {
262                let year = ob.getattr(intern!(ob.py(), "year"))?.extract()?;
263                let month: u8 = ob.getattr(intern!(ob.py(), "month"))?.extract()?;
264                let day = ob.getattr(intern!(ob.py(), "day"))?.extract()?;
265                (year, month, day)
266            }
267        };
268
269        // Convert the month number to time::Month enum
270        let month = month_from_number!(month);
271
272        Date::from_calendar_date(year, month, day)
273            .map_err(|_| PyValueError::new_err("invalid or out-of-range date"))
274    }
275}
276
277impl<'py> IntoPyObject<'py> for Time {
278    type Target = PyTime;
279    type Output = Bound<'py, Self::Target>;
280    type Error = PyErr;
281
282    #[cfg(feature = "experimental-inspect")]
283    const OUTPUT_TYPE: PyStaticExpr = PyTime::TYPE_HINT;
284
285    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
286        let hour = self.hour();
287        let minute = self.minute();
288        let second = self.second();
289        let microsecond = self.microsecond();
290
291        PyTime::new(py, hour, minute, second, microsecond, None)
292    }
293}
294
295impl FromPyObject<'_, '_> for Time {
296    type Error = PyErr;
297
298    #[cfg(feature = "experimental-inspect")]
299    const INPUT_TYPE: PyStaticExpr = PyTime::TYPE_HINT;
300
301    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
302        let (hour, minute, second, microsecond) = {
303            #[cfg(not(Py_LIMITED_API))]
304            {
305                let time = ob.cast::<PyTime>()?;
306                let hour: u8 = time.get_hour();
307                let minute: u8 = time.get_minute();
308                let second: u8 = time.get_second();
309                let microsecond = time.get_microsecond();
310                (hour, minute, second, microsecond)
311            }
312
313            #[cfg(Py_LIMITED_API)]
314            {
315                let hour: u8 = ob.getattr(intern!(ob.py(), "hour"))?.extract()?;
316                let minute: u8 = ob.getattr(intern!(ob.py(), "minute"))?.extract()?;
317                let second: u8 = ob.getattr(intern!(ob.py(), "second"))?.extract()?;
318                let microsecond = ob.getattr(intern!(ob.py(), "microsecond"))?.extract()?;
319                (hour, minute, second, microsecond)
320            }
321        };
322
323        Time::from_hms_micro(hour, minute, second, microsecond)
324            .map_err(|_| PyValueError::new_err("invalid or out-of-range time"))
325    }
326}
327
328impl<'py> IntoPyObject<'py> for PrimitiveDateTime {
329    type Target = PyDateTime;
330    type Output = Bound<'py, Self::Target>;
331    type Error = PyErr;
332
333    #[cfg(feature = "experimental-inspect")]
334    const OUTPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
335
336    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
337        let date = self.date();
338        let time = self.time();
339
340        let year = date.year();
341        let month = date.month() as u8;
342        let day = date.day();
343        let hour = time.hour();
344        let minute = time.minute();
345        let second = time.second();
346        let microsecond = time.microsecond();
347
348        PyDateTime::new(
349            py,
350            year,
351            month,
352            day,
353            hour,
354            minute,
355            second,
356            microsecond,
357            None,
358        )
359    }
360}
361
362impl FromPyObject<'_, '_> for PrimitiveDateTime {
363    type Error = PyErr;
364
365    #[cfg(feature = "experimental-inspect")]
366    const INPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
367
368    fn extract(dt: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
369        let has_tzinfo = {
370            #[cfg(not(Py_LIMITED_API))]
371            {
372                let dt = dt.cast::<PyDateTime>()?;
373                dt.get_tzinfo().is_some()
374            }
375            #[cfg(Py_LIMITED_API)]
376            {
377                !dt.getattr(intern!(dt.py(), "tzinfo"))?.is_none()
378            }
379        };
380
381        if has_tzinfo {
382            return Err(PyTypeError::new_err("expected a datetime without tzinfo"));
383        }
384
385        let (date, time) = extract_date_time(&dt)?;
386
387        Ok(PrimitiveDateTime::new(date, time))
388    }
389}
390
391impl<'py> IntoPyObject<'py> for UtcOffset {
392    type Target = PyTzInfo;
393    type Output = Bound<'py, Self::Target>;
394    type Error = PyErr;
395
396    #[cfg(feature = "experimental-inspect")]
397    const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("datetime", "timezone");
398
399    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
400        // Get offset in seconds
401        let seconds_offset = self.whole_seconds();
402        let td = PyDelta::new(py, 0, seconds_offset, 0, true)?;
403        PyTzInfo::fixed_offset(py, td)
404    }
405}
406
407impl FromPyObject<'_, '_> for UtcOffset {
408    type Error = PyErr;
409
410    #[cfg(feature = "experimental-inspect")]
411    const INPUT_TYPE: PyStaticExpr = PyTzInfo::TYPE_HINT;
412
413    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
414        #[cfg(not(Py_LIMITED_API))]
415        let ob = ob.cast::<PyTzInfo>()?;
416
417        // Get the offset in seconds from the Python tzinfo
418        let py_timedelta = ob.call_method1("utcoffset", (PyNone::get(ob.py()),))?;
419        if py_timedelta.is_none() {
420            return Err(PyTypeError::new_err(format!(
421                "{ob:?} is not a fixed offset timezone"
422            )));
423        }
424
425        let total_seconds: Duration = py_timedelta.extract()?;
426        let seconds = total_seconds.whole_seconds();
427
428        // Create the UtcOffset from the seconds
429        UtcOffset::from_whole_seconds(seconds as i32)
430            .map_err(|_| PyValueError::new_err("UTC offset out of bounds"))
431    }
432}
433
434impl<'py> IntoPyObject<'py> for OffsetDateTime {
435    type Target = PyDateTime;
436    type Output = Bound<'py, Self::Target>;
437    type Error = PyErr;
438
439    #[cfg(feature = "experimental-inspect")]
440    const OUTPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
441
442    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
443        let date = self.date();
444        let time = self.time();
445        let offset = self.offset();
446
447        // Convert the offset to a Python tzinfo
448        let py_tzinfo = offset.into_pyobject(py)?;
449
450        let year = date.year();
451        let month = date.month() as u8;
452        let day = date.day();
453        let hour = time.hour();
454        let minute = time.minute();
455        let second = time.second();
456        let microsecond = time.microsecond();
457
458        PyDateTime::new(
459            py,
460            year,
461            month,
462            day,
463            hour,
464            minute,
465            second,
466            microsecond,
467            Some(py_tzinfo.cast()?),
468        )
469    }
470}
471
472impl FromPyObject<'_, '_> for OffsetDateTime {
473    type Error = PyErr;
474
475    #[cfg(feature = "experimental-inspect")]
476    const INPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
477
478    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
479        let offset: UtcOffset = {
480            #[cfg(not(Py_LIMITED_API))]
481            {
482                let dt = ob.cast::<PyDateTime>()?;
483                let tzinfo = dt.get_tzinfo().ok_or_else(|| {
484                    PyTypeError::new_err("expected a datetime with non-None tzinfo")
485                })?;
486                tzinfo.extract()?
487            }
488            #[cfg(Py_LIMITED_API)]
489            {
490                let tzinfo = ob.getattr(intern!(ob.py(), "tzinfo"))?;
491                if tzinfo.is_none() {
492                    return Err(PyTypeError::new_err(
493                        "expected a datetime with non-None tzinfo",
494                    ));
495                }
496                tzinfo.extract()?
497            }
498        };
499
500        let (date, time) = extract_date_time(&ob)?;
501
502        let primitive_dt = PrimitiveDateTime::new(date, time);
503        Ok(primitive_dt.assume_offset(offset))
504    }
505}
506
507impl<'py> IntoPyObject<'py> for UtcDateTime {
508    type Target = PyDateTime;
509    type Output = Bound<'py, Self::Target>;
510    type Error = PyErr;
511
512    #[cfg(feature = "experimental-inspect")]
513    const OUTPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
514
515    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
516        let date = self.date();
517        let time = self.time();
518
519        let py_tzinfo = PyTzInfo::utc(py)?;
520
521        let year = date.year();
522        let month = date.month() as u8;
523        let day = date.day();
524        let hour = time.hour();
525        let minute = time.minute();
526        let second = time.second();
527        let microsecond = time.microsecond();
528
529        PyDateTime::new(
530            py,
531            year,
532            month,
533            day,
534            hour,
535            minute,
536            second,
537            microsecond,
538            Some(&py_tzinfo),
539        )
540    }
541}
542
543impl FromPyObject<'_, '_> for UtcDateTime {
544    type Error = PyErr;
545
546    #[cfg(feature = "experimental-inspect")]
547    const INPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
548
549    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
550        let tzinfo = {
551            #[cfg(not(Py_LIMITED_API))]
552            {
553                let dt = ob.cast::<PyDateTime>()?;
554                dt.get_tzinfo().ok_or_else(|| {
555                    PyTypeError::new_err("expected a datetime with non-None tzinfo")
556                })?
557            }
558
559            #[cfg(Py_LIMITED_API)]
560            {
561                let tzinfo = ob.getattr(intern!(ob.py(), "tzinfo"))?;
562                if tzinfo.is_none() {
563                    return Err(PyTypeError::new_err(
564                        "expected a datetime with non-None tzinfo",
565                    ));
566                }
567                tzinfo
568            }
569        };
570
571        // Verify that the tzinfo is UTC
572        let is_utc = tzinfo.eq(PyTzInfo::utc(ob.py())?)?;
573
574        if !is_utc {
575            return Err(PyValueError::new_err(
576                "expected a datetime with UTC timezone",
577            ));
578        }
579
580        let (date, time) = extract_date_time(&ob)?;
581        let primitive_dt = PrimitiveDateTime::new(date, time);
582        Ok(primitive_dt.assume_utc().into())
583    }
584}
585
586impl_into_py_for_ref!(Duration, PyDelta);
587impl_into_py_for_ref!(Date, PyDate);
588impl_into_py_for_ref!(Time, PyTime);
589impl_into_py_for_ref!(PrimitiveDateTime, PyDateTime);
590impl_into_py_for_ref!(UtcOffset, PyTzInfo);
591impl_into_py_for_ref!(OffsetDateTime, PyDateTime);
592impl_into_py_for_ref!(UtcDateTime, PyDateTime);
593
594#[cfg(test)]
595mod tests {
596    use super::*;
597    use crate::intern;
598    use crate::types::any::PyAnyMethods;
599    use crate::types::PyTypeMethods;
600
601    mod utils {
602        use super::*;
603
604        pub(crate) fn extract_py_delta_from_duration(
605            duration: Duration,
606            py: Python<'_>,
607        ) -> (i64, i64, i64) {
608            let py_delta = duration.into_pyobject(py).unwrap();
609            let days = py_delta
610                .getattr(intern!(py, "days"))
611                .unwrap()
612                .extract::<i64>()
613                .unwrap();
614            let seconds = py_delta
615                .getattr(intern!(py, "seconds"))
616                .unwrap()
617                .extract::<i64>()
618                .unwrap();
619            let microseconds = py_delta
620                .getattr(intern!(py, "microseconds"))
621                .unwrap()
622                .extract::<i64>()
623                .unwrap();
624            (days, seconds, microseconds)
625        }
626
627        pub(crate) fn extract_py_date_from_date(date: Date, py: Python<'_>) -> (i32, u8, u8) {
628            let py_date = date.into_pyobject(py).unwrap();
629
630            // Check the Python object is correct
631            let year = py_date
632                .getattr(intern!(py, "year"))
633                .unwrap()
634                .extract::<i32>()
635                .unwrap();
636            let month = py_date
637                .getattr(intern!(py, "month"))
638                .unwrap()
639                .extract::<u8>()
640                .unwrap();
641            let day = py_date
642                .getattr(intern!(py, "day"))
643                .unwrap()
644                .extract::<u8>()
645                .unwrap();
646            (year, month, day)
647        }
648
649        pub(crate) fn create_date_from_py_date(
650            py: Python<'_>,
651            year: i32,
652            month: u8,
653            day: u8,
654        ) -> PyResult<Date> {
655            let datetime = py.import("datetime").unwrap();
656            let date_type = datetime.getattr(intern!(py, "date")).unwrap();
657            let py_date = date_type.call1((year, month, day));
658            match py_date {
659                Ok(py_date) => py_date.extract(),
660                Err(err) => Err(err),
661            }
662        }
663
664        pub(crate) fn create_time_from_py_time(
665            py: Python<'_>,
666            hour: u8,
667            minute: u8,
668            second: u8,
669            microseocnd: u32,
670        ) -> PyResult<Time> {
671            let datetime = py.import("datetime").unwrap();
672            let time_type = datetime.getattr(intern!(py, "time")).unwrap();
673            let py_time = time_type.call1((hour, minute, second, microseocnd));
674            match py_time {
675                Ok(py_time) => py_time.extract(),
676                Err(err) => Err(err),
677            }
678        }
679
680        pub(crate) fn extract_py_time_from_time(time: Time, py: Python<'_>) -> (u8, u8, u8, u32) {
681            let py_time = time.into_pyobject(py).unwrap();
682            let hour = py_time
683                .getattr(intern!(py, "hour"))
684                .unwrap()
685                .extract::<u8>()
686                .unwrap();
687            let minute = py_time
688                .getattr(intern!(py, "minute"))
689                .unwrap()
690                .extract::<u8>()
691                .unwrap();
692            let second = py_time
693                .getattr(intern!(py, "second"))
694                .unwrap()
695                .extract::<u8>()
696                .unwrap();
697            let microsecond = py_time
698                .getattr(intern!(py, "microsecond"))
699                .unwrap()
700                .extract::<u32>()
701                .unwrap();
702            (hour, minute, second, microsecond)
703        }
704
705        pub(crate) fn extract_date_time_from_primitive_date_time(
706            dt: PrimitiveDateTime,
707            py: Python<'_>,
708        ) -> (u32, u8, u8, u8, u8, u8, u32) {
709            let py_dt = dt.into_pyobject(py).unwrap();
710            let year = py_dt
711                .getattr(intern!(py, "year"))
712                .unwrap()
713                .extract::<u32>()
714                .unwrap();
715            let month = py_dt
716                .getattr(intern!(py, "month"))
717                .unwrap()
718                .extract::<u8>()
719                .unwrap();
720            let day = py_dt
721                .getattr(intern!(py, "day"))
722                .unwrap()
723                .extract::<u8>()
724                .unwrap();
725            let hour = py_dt
726                .getattr(intern!(py, "hour"))
727                .unwrap()
728                .extract::<u8>()
729                .unwrap();
730            let minute = py_dt
731                .getattr(intern!(py, "minute"))
732                .unwrap()
733                .extract::<u8>()
734                .unwrap();
735            let second = py_dt
736                .getattr(intern!(py, "second"))
737                .unwrap()
738                .extract::<u8>()
739                .unwrap();
740            let microsecond = py_dt
741                .getattr(intern!(py, "microsecond"))
742                .unwrap()
743                .extract::<u32>()
744                .unwrap();
745            (year, month, day, hour, minute, second, microsecond)
746        }
747
748        #[expect(clippy::too_many_arguments)]
749        pub(crate) fn create_primitive_date_time_from_py(
750            py: Python<'_>,
751            year: u32,
752            month: u8,
753            day: u8,
754            hour: u8,
755            minute: u8,
756            second: u8,
757            microsecond: u32,
758        ) -> PyResult<PrimitiveDateTime> {
759            let datetime = py.import("datetime").unwrap();
760            let datetime_type = datetime.getattr(intern!(py, "datetime")).unwrap();
761            let py_dt = datetime_type.call1((year, month, day, hour, minute, second, microsecond));
762            match py_dt {
763                Ok(py_dt) => py_dt.extract(),
764                Err(err) => Err(err),
765            }
766        }
767
768        pub(crate) fn extract_total_seconds_from_utcoffset(
769            offset: UtcOffset,
770            py: Python<'_>,
771        ) -> f64 {
772            let py_tz = offset.into_pyobject(py).unwrap();
773            let utc_offset = py_tz.call_method1("utcoffset", (py.None(),)).unwrap();
774            let total_seconds = utc_offset
775                .getattr(intern!(py, "total_seconds"))
776                .unwrap()
777                .call0()
778                .unwrap()
779                .extract::<f64>()
780                .unwrap();
781            total_seconds
782        }
783
784        pub(crate) fn extract_from_utc_date_time(
785            dt: UtcDateTime,
786            py: Python<'_>,
787        ) -> (u32, u8, u8, u8, u8, u8, u32) {
788            let py_dt = dt.into_pyobject(py).unwrap();
789            let year = py_dt
790                .getattr(intern!(py, "year"))
791                .unwrap()
792                .extract::<u32>()
793                .unwrap();
794            let month = py_dt
795                .getattr(intern!(py, "month"))
796                .unwrap()
797                .extract::<u8>()
798                .unwrap();
799            let day = py_dt
800                .getattr(intern!(py, "day"))
801                .unwrap()
802                .extract::<u8>()
803                .unwrap();
804            let hour = py_dt
805                .getattr(intern!(py, "hour"))
806                .unwrap()
807                .extract::<u8>()
808                .unwrap();
809            let minute = py_dt
810                .getattr(intern!(py, "minute"))
811                .unwrap()
812                .extract::<u8>()
813                .unwrap();
814            let second = py_dt
815                .getattr(intern!(py, "second"))
816                .unwrap()
817                .extract::<u8>()
818                .unwrap();
819            let microsecond = py_dt
820                .getattr(intern!(py, "microsecond"))
821                .unwrap()
822                .extract::<u32>()
823                .unwrap();
824            (year, month, day, hour, minute, second, microsecond)
825        }
826    }
827    #[test]
828    fn test_time_duration_conversion() {
829        Python::attach(|py| {
830            // Regular duration
831            let duration = Duration::new(1, 500_000_000); // 1.5 seconds
832            let (_, seconds, microseconds) = utils::extract_py_delta_from_duration(duration, py);
833            assert_eq!(seconds, 1);
834            assert_eq!(microseconds, 500_000);
835
836            // Check negative durations
837            let neg_duration = Duration::new(-10, 0); // -10 seconds
838            let (days, seconds, _) = utils::extract_py_delta_from_duration(neg_duration, py);
839            assert_eq!(days, -1);
840            assert_eq!(seconds, 86390); // 86400 - 10 seconds
841
842            // Test case for exact negative days (should use normal division path)
843            let exact_day = Duration::seconds(-86_400); // Exactly -1 day
844            let (days, seconds, microseconds) =
845                utils::extract_py_delta_from_duration(exact_day, py);
846            assert_eq!(days, -1);
847            assert_eq!(seconds, 0);
848            assert_eq!(microseconds, 0);
849        });
850    }
851
852    #[test]
853    fn test_time_duration_conversion_large_values() {
854        Python::attach(|py| {
855            // Large duration (close to max)
856            let large_duration = Duration::seconds(86_399_999_000_000); // Almost max
857            let (days, _, _) = utils::extract_py_delta_from_duration(large_duration, py);
858            assert!(days > 999_000_000);
859
860            // Test over limit (should yield Overflow error in python)
861            let too_large = Duration::seconds(86_400_000_000_000); // Over max
862            let result = too_large.into_pyobject(py);
863            assert!(result.is_err());
864            let err_type = result.unwrap_err().get_type(py).name().unwrap();
865            assert_eq!(err_type, "OverflowError");
866
867            for duration in [Duration::MIN, Duration::MAX] {
868                let err = duration.into_pyobject(py).unwrap_err();
869                assert_eq!(err.get_type(py).name().unwrap(), "OverflowError");
870            }
871        });
872    }
873
874    #[test]
875    fn test_time_duration_nanosecond_resolution() {
876        Python::attach(|py| {
877            // Test nanosecond conversion to microseconds
878            let duration = Duration::new(0, 1_234_567);
879            let (_, _, microseconds) = utils::extract_py_delta_from_duration(duration, py);
880            // Python timedelta only has microsecond resolution, so we should get 1234 microseconds
881            assert_eq!(microseconds, 1234);
882        });
883    }
884
885    #[test]
886    fn test_time_duration_from_python() {
887        Python::attach(|py| {
888            // Create Python timedeltas with various values
889            let datetime = py.import("datetime").unwrap();
890            let timedelta = datetime.getattr(intern!(py, "timedelta")).unwrap();
891
892            // Test positive values
893            let py_delta1 = timedelta.call1((3, 7200, 500000)).unwrap();
894            let duration1: Duration = py_delta1.extract().unwrap();
895            assert_eq!(duration1.whole_days(), 3);
896            assert_eq!(duration1.whole_seconds() % 86400, 7200);
897            assert_eq!(duration1.subsec_nanoseconds(), 500000000);
898
899            // Test negative days
900            let py_delta2 = timedelta.call1((-2, 43200)).unwrap();
901            let duration2: Duration = py_delta2.extract().unwrap();
902            assert_eq!(duration2.whole_days(), -1);
903            assert_eq!(duration2.whole_seconds(), -129600);
904        });
905    }
906
907    #[test]
908    fn test_time_date_conversion() {
909        Python::attach(|py| {
910            // Regular date
911            let date = Date::from_calendar_date(2023, Month::April, 15).unwrap();
912            let (year, month, day) = utils::extract_py_date_from_date(date, py);
913            assert_eq!(year, 2023);
914            assert_eq!(month, 4);
915            assert_eq!(day, 15);
916
917            // Test edge cases
918            let min_date = Date::from_calendar_date(1, Month::January, 1).unwrap();
919            let (min_year, min_month, min_day) = utils::extract_py_date_from_date(min_date, py);
920            assert_eq!(min_year, 1);
921            assert_eq!(min_month, 1);
922            assert_eq!(min_day, 1);
923
924            let max_date = Date::from_calendar_date(9999, Month::December, 31).unwrap();
925            let (max_year, max_month, max_day) = utils::extract_py_date_from_date(max_date, py);
926            assert_eq!(max_year, 9999);
927            assert_eq!(max_month, 12);
928            assert_eq!(max_day, 31);
929        });
930    }
931
932    #[test]
933    fn test_time_date_from_python() {
934        Python::attach(|py| {
935            let date1 = utils::create_date_from_py_date(py, 2023, 4, 15).unwrap();
936            assert_eq!(date1.year(), 2023);
937            assert_eq!(date1.month(), Month::April);
938            assert_eq!(date1.day(), 15);
939
940            // Test min date
941            let date2 = utils::create_date_from_py_date(py, 1, 1, 1).unwrap();
942            assert_eq!(date2.year(), 1);
943            assert_eq!(date2.month(), Month::January);
944            assert_eq!(date2.day(), 1);
945
946            // Test max date
947            let date3 = utils::create_date_from_py_date(py, 9999, 12, 31).unwrap();
948            assert_eq!(date3.year(), 9999);
949            assert_eq!(date3.month(), Month::December);
950            assert_eq!(date3.day(), 31);
951
952            // Test leap year date
953            let date4 = utils::create_date_from_py_date(py, 2024, 2, 29).unwrap();
954            assert_eq!(date4.year(), 2024);
955            assert_eq!(date4.month(), Month::February);
956            assert_eq!(date4.day(), 29);
957        });
958    }
959
960    #[test]
961    fn test_time_date_invalid_values() {
962        Python::attach(|py| {
963            let invalid_date = utils::create_date_from_py_date(py, 2023, 2, 30);
964            assert!(invalid_date.is_err());
965
966            // Test extraction of invalid month
967            let another_invalid_date = utils::create_date_from_py_date(py, 2023, 13, 1);
968            assert!(another_invalid_date.is_err());
969        });
970    }
971
972    #[test]
973    fn test_time_time_conversion() {
974        Python::attach(|py| {
975            // Regular time
976            let time = Time::from_hms_micro(14, 30, 45, 123456).unwrap();
977            let (hour, minute, second, microsecond) = utils::extract_py_time_from_time(time, py);
978            assert_eq!(hour, 14);
979            assert_eq!(minute, 30);
980            assert_eq!(second, 45);
981            assert_eq!(microsecond, 123456);
982
983            // Test edge cases
984            let min_time = Time::from_hms_micro(0, 0, 0, 0).unwrap();
985            let (min_hour, min_minute, min_second, min_microsecond) =
986                utils::extract_py_time_from_time(min_time, py);
987            assert_eq!(min_hour, 0);
988            assert_eq!(min_minute, 0);
989            assert_eq!(min_second, 0);
990            assert_eq!(min_microsecond, 0);
991
992            let max_time = Time::from_hms_micro(23, 59, 59, 999999).unwrap();
993            let (max_hour, max_minute, max_second, max_microsecond) =
994                utils::extract_py_time_from_time(max_time, py);
995            assert_eq!(max_hour, 23);
996            assert_eq!(max_minute, 59);
997            assert_eq!(max_second, 59);
998            assert_eq!(max_microsecond, 999999);
999        });
1000    }
1001
1002    #[test]
1003    fn test_time_time_from_python() {
1004        Python::attach(|py| {
1005            let time1 = utils::create_time_from_py_time(py, 14, 30, 45, 123456).unwrap();
1006            assert_eq!(time1.hour(), 14);
1007            assert_eq!(time1.minute(), 30);
1008            assert_eq!(time1.second(), 45);
1009            assert_eq!(time1.microsecond(), 123456);
1010
1011            // Test min time
1012            let time2 = utils::create_time_from_py_time(py, 0, 0, 0, 0).unwrap();
1013            assert_eq!(time2.hour(), 0);
1014            assert_eq!(time2.minute(), 0);
1015            assert_eq!(time2.second(), 0);
1016            assert_eq!(time2.microsecond(), 0);
1017
1018            // Test max time
1019            let time3 = utils::create_time_from_py_time(py, 23, 59, 59, 999999).unwrap();
1020            assert_eq!(time3.hour(), 23);
1021            assert_eq!(time3.minute(), 59);
1022            assert_eq!(time3.second(), 59);
1023            assert_eq!(time3.microsecond(), 999999);
1024        });
1025    }
1026
1027    #[test]
1028    fn test_time_time_invalid_values() {
1029        Python::attach(|py| {
1030            let result = utils::create_time_from_py_time(py, 24, 0, 0, 0);
1031            assert!(result.is_err());
1032            let result = utils::create_time_from_py_time(py, 12, 60, 0, 0);
1033            assert!(result.is_err());
1034            let result = utils::create_time_from_py_time(py, 12, 30, 60, 0);
1035            assert!(result.is_err());
1036            let result = utils::create_time_from_py_time(py, 12, 30, 30, 1000000);
1037            assert!(result.is_err());
1038        });
1039    }
1040
1041    #[test]
1042    fn test_time_time_with_timezone() {
1043        Python::attach(|py| {
1044            // Create Python time with timezone (just to ensure we can handle it properly)
1045            let datetime = py.import("datetime").unwrap();
1046            let time_type = datetime.getattr(intern!(py, "time")).unwrap();
1047            let tz_utc = PyTzInfo::utc(py).unwrap();
1048
1049            // Create time with timezone
1050            let py_time_with_tz = time_type.call1((12, 30, 45, 0, tz_utc)).unwrap();
1051            let time: Time = py_time_with_tz.extract().unwrap();
1052
1053            assert_eq!(time.hour(), 12);
1054            assert_eq!(time.minute(), 30);
1055            assert_eq!(time.second(), 45);
1056        });
1057    }
1058
1059    #[test]
1060    fn test_time_primitive_datetime_conversion() {
1061        Python::attach(|py| {
1062            // Regular datetime
1063            let date = Date::from_calendar_date(2023, Month::April, 15).unwrap();
1064            let time = Time::from_hms_micro(14, 30, 45, 123456).unwrap();
1065            let dt = PrimitiveDateTime::new(date, time);
1066            let (year, month, day, hour, minute, second, microsecond) =
1067                utils::extract_date_time_from_primitive_date_time(dt, py);
1068
1069            assert_eq!(year, 2023);
1070            assert_eq!(month, 4);
1071            assert_eq!(day, 15);
1072            assert_eq!(hour, 14);
1073            assert_eq!(minute, 30);
1074            assert_eq!(second, 45);
1075            assert_eq!(microsecond, 123456);
1076
1077            // Test min datetime
1078            let min_date = Date::from_calendar_date(1, Month::January, 1).unwrap();
1079            let min_time = Time::from_hms_micro(0, 0, 0, 0).unwrap();
1080            let min_dt = PrimitiveDateTime::new(min_date, min_time);
1081            let (year, month, day, hour, minute, second, microsecond) =
1082                utils::extract_date_time_from_primitive_date_time(min_dt, py);
1083            assert_eq!(year, 1);
1084            assert_eq!(month, 1);
1085            assert_eq!(day, 1);
1086            assert_eq!(hour, 0);
1087            assert_eq!(minute, 0);
1088            assert_eq!(second, 0);
1089            assert_eq!(microsecond, 0);
1090        });
1091    }
1092
1093    #[test]
1094    fn test_time_primitive_datetime_from_python() {
1095        Python::attach(|py| {
1096            let dt1 =
1097                utils::create_primitive_date_time_from_py(py, 2023, 4, 15, 14, 30, 45, 123456)
1098                    .unwrap();
1099            assert_eq!(dt1.year(), 2023);
1100            assert_eq!(dt1.month(), Month::April);
1101            assert_eq!(dt1.day(), 15);
1102            assert_eq!(dt1.hour(), 14);
1103            assert_eq!(dt1.minute(), 30);
1104            assert_eq!(dt1.second(), 45);
1105            assert_eq!(dt1.microsecond(), 123456);
1106
1107            let dt2 = utils::create_primitive_date_time_from_py(py, 1, 1, 1, 0, 0, 0, 0).unwrap();
1108            assert_eq!(dt2.year(), 1);
1109            assert_eq!(dt2.month(), Month::January);
1110            assert_eq!(dt2.day(), 1);
1111            assert_eq!(dt2.hour(), 0);
1112            assert_eq!(dt2.minute(), 0);
1113        });
1114    }
1115
1116    #[test]
1117    fn test_time_utc_offset_conversion() {
1118        Python::attach(|py| {
1119            // Test positive offset
1120            let offset = UtcOffset::from_hms(5, 30, 0).unwrap();
1121            let total_seconds = utils::extract_total_seconds_from_utcoffset(offset, py);
1122            assert_eq!(total_seconds, 5.0 * 3600.0 + 30.0 * 60.0);
1123
1124            // Test negative offset
1125            let neg_offset = UtcOffset::from_hms(-8, -15, 0).unwrap();
1126            let neg_total_seconds = utils::extract_total_seconds_from_utcoffset(neg_offset, py);
1127            assert_eq!(neg_total_seconds, -8.0 * 3600.0 - 15.0 * 60.0);
1128        });
1129    }
1130
1131    #[test]
1132    fn test_time_utc_offset_from_python() {
1133        Python::attach(|py| {
1134            // Create timezone objects
1135            let datetime = py.import("datetime").unwrap();
1136            let timezone = datetime.getattr(intern!(py, "timezone")).unwrap();
1137            let timedelta = datetime.getattr(intern!(py, "timedelta")).unwrap();
1138
1139            // Test UTC
1140            let tz_utc = PyTzInfo::utc(py).unwrap();
1141            let utc_offset: UtcOffset = tz_utc.extract().unwrap();
1142            assert_eq!(utc_offset.whole_hours(), 0);
1143            assert_eq!(utc_offset.minutes_past_hour(), 0);
1144            assert_eq!(utc_offset.seconds_past_minute(), 0);
1145
1146            // Test positive offset
1147            let td_pos = timedelta.call1((0, 19800, 0)).unwrap(); // 5 hours 30 minutes
1148            let tz_pos = timezone.call1((td_pos,)).unwrap();
1149            let offset_pos: UtcOffset = tz_pos.extract().unwrap();
1150            assert_eq!(offset_pos.whole_hours(), 5);
1151            assert_eq!(offset_pos.minutes_past_hour(), 30);
1152
1153            // Test negative offset
1154            let td_neg = timedelta.call1((0, -30900, 0)).unwrap(); // -8 hours -35 minutes
1155            let tz_neg = timezone.call1((td_neg,)).unwrap();
1156            let offset_neg: UtcOffset = tz_neg.extract().unwrap();
1157            assert_eq!(offset_neg.whole_hours(), -8);
1158            assert_eq!(offset_neg.minutes_past_hour(), -35);
1159        });
1160    }
1161
1162    #[test]
1163    fn test_time_offset_datetime_conversion() {
1164        Python::attach(|py| {
1165            // Create an OffsetDateTime with +5:30 offset
1166            let date = Date::from_calendar_date(2023, Month::April, 15).unwrap();
1167            let time = Time::from_hms_micro(14, 30, 45, 123456).unwrap();
1168            let offset = UtcOffset::from_hms(5, 30, 0).unwrap();
1169            let dt = PrimitiveDateTime::new(date, time).assume_offset(offset);
1170
1171            // Convert to Python
1172            let py_dt = dt.into_pyobject(py).unwrap();
1173
1174            // Check components
1175            let year = py_dt
1176                .getattr(intern!(py, "year"))
1177                .unwrap()
1178                .extract::<i32>()
1179                .unwrap();
1180            let month = py_dt
1181                .getattr(intern!(py, "month"))
1182                .unwrap()
1183                .extract::<u8>()
1184                .unwrap();
1185            let day = py_dt
1186                .getattr(intern!(py, "day"))
1187                .unwrap()
1188                .extract::<u8>()
1189                .unwrap();
1190            let hour = py_dt
1191                .getattr(intern!(py, "hour"))
1192                .unwrap()
1193                .extract::<u8>()
1194                .unwrap();
1195            let minute = py_dt
1196                .getattr(intern!(py, "minute"))
1197                .unwrap()
1198                .extract::<u8>()
1199                .unwrap();
1200            let second = py_dt
1201                .getattr(intern!(py, "second"))
1202                .unwrap()
1203                .extract::<u8>()
1204                .unwrap();
1205            let microsecond = py_dt
1206                .getattr(intern!(py, "microsecond"))
1207                .unwrap()
1208                .extract::<u32>()
1209                .unwrap();
1210
1211            assert_eq!(year, 2023);
1212            assert_eq!(month, 4);
1213            assert_eq!(day, 15);
1214            assert_eq!(hour, 14);
1215            assert_eq!(minute, 30);
1216            assert_eq!(second, 45);
1217            assert_eq!(microsecond, 123456);
1218
1219            // Check timezone offset
1220            let tzinfo = py_dt.getattr(intern!(py, "tzinfo")).unwrap();
1221            let utcoffset = tzinfo.call_method1("utcoffset", (py_dt,)).unwrap();
1222            let seconds = utcoffset
1223                .call_method0("total_seconds")
1224                .unwrap()
1225                .extract::<f64>()
1226                .unwrap();
1227            assert_eq!(seconds, 5.0 * 3600.0 + 30.0 * 60.0);
1228        });
1229    }
1230
1231    #[test]
1232    fn test_time_offset_datetime_from_python() {
1233        Python::attach(|py| {
1234            // Create Python datetime with timezone
1235            let datetime = py.import("datetime").unwrap();
1236            let datetime_type = datetime.getattr(intern!(py, "datetime")).unwrap();
1237            let timezone = datetime.getattr(intern!(py, "timezone")).unwrap();
1238            let timedelta = datetime.getattr(intern!(py, "timedelta")).unwrap();
1239
1240            // Create a timezone (+5:30)
1241            let td = timedelta.call1((0, 19800, 0)).unwrap(); // 5:30:00
1242            let tz = timezone.call1((td,)).unwrap();
1243
1244            // Create datetime with this timezone
1245            let py_dt = datetime_type
1246                .call1((2023, 4, 15, 14, 30, 45, 123456, tz))
1247                .unwrap();
1248
1249            // Extract to Rust
1250            let dt: OffsetDateTime = py_dt.extract().unwrap();
1251
1252            // Verify components
1253            assert_eq!(dt.year(), 2023);
1254            assert_eq!(dt.month(), Month::April);
1255            assert_eq!(dt.day(), 15);
1256            assert_eq!(dt.hour(), 14);
1257            assert_eq!(dt.minute(), 30);
1258            assert_eq!(dt.second(), 45);
1259            assert_eq!(dt.microsecond(), 123456);
1260            assert_eq!(dt.offset().whole_hours(), 5);
1261            assert_eq!(dt.offset().minutes_past_hour(), 30);
1262        });
1263    }
1264
1265    #[test]
1266    fn test_time_utc_datetime_conversion() {
1267        Python::attach(|py| {
1268            let date = Date::from_calendar_date(2023, Month::April, 15).unwrap();
1269            let time = Time::from_hms_micro(14, 30, 45, 123456).unwrap();
1270            let primitive_dt = PrimitiveDateTime::new(date, time);
1271            let dt: UtcDateTime = primitive_dt.assume_utc().into();
1272            let (year, month, day, hour, minute, second, microsecond) =
1273                utils::extract_from_utc_date_time(dt, py);
1274
1275            assert_eq!(year, 2023);
1276            assert_eq!(month, 4);
1277            assert_eq!(day, 15);
1278            assert_eq!(hour, 14);
1279            assert_eq!(minute, 30);
1280            assert_eq!(second, 45);
1281            assert_eq!(microsecond, 123456);
1282        });
1283    }
1284
1285    #[test]
1286    fn test_time_utc_datetime_from_python() {
1287        Python::attach(|py| {
1288            // Create Python UTC datetime
1289            let datetime = py.import("datetime").unwrap();
1290            let datetime_type = datetime.getattr(intern!(py, "datetime")).unwrap();
1291            let tz_utc = PyTzInfo::utc(py).unwrap();
1292
1293            // Create datetime with UTC timezone
1294            let py_dt = datetime_type
1295                .call1((2023, 4, 15, 14, 30, 45, 123456, tz_utc))
1296                .unwrap();
1297
1298            // Convert to Rust
1299            let dt: UtcDateTime = py_dt.extract().unwrap();
1300
1301            // Verify components
1302            assert_eq!(dt.year(), 2023);
1303            assert_eq!(dt.month(), Month::April);
1304            assert_eq!(dt.day(), 15);
1305            assert_eq!(dt.hour(), 14);
1306            assert_eq!(dt.minute(), 30);
1307            assert_eq!(dt.second(), 45);
1308            assert_eq!(dt.microsecond(), 123456);
1309        });
1310    }
1311
1312    #[test]
1313    fn test_time_utc_datetime_non_utc_timezone() {
1314        Python::attach(|py| {
1315            // Create Python datetime with non-UTC timezone
1316            let datetime = py.import("datetime").unwrap();
1317            let datetime_type = datetime.getattr(intern!(py, "datetime")).unwrap();
1318            let timezone = datetime.getattr(intern!(py, "timezone")).unwrap();
1319            let timedelta = datetime.getattr(intern!(py, "timedelta")).unwrap();
1320
1321            // Create a non-UTC timezone (EST = UTC-5)
1322            let td = timedelta.call1((0, -18000, 0)).unwrap(); // -5 hours
1323            let tz_est = timezone.call1((td,)).unwrap();
1324
1325            // Create datetime with EST timezone
1326            let py_dt = datetime_type
1327                .call1((2023, 4, 15, 14, 30, 45, 123456, tz_est))
1328                .unwrap();
1329
1330            // Try to convert to UtcDateTime - should fail
1331            let result: Result<UtcDateTime, _> = py_dt.extract();
1332            assert!(result.is_err());
1333        });
1334    }
1335
1336    #[cfg(not(any(target_arch = "wasm32", Py_GIL_DISABLED)))]
1337    mod proptests {
1338        use super::*;
1339        use proptest::proptest;
1340
1341        proptest! {
1342            #[test]
1343            fn test_time_duration_roundtrip(days in -9999i64..=9999i64, seconds in -86399i64..=86399i64, microseconds in -999999i64..=999999i64) {
1344                // Generate a valid duration that should roundtrip successfully
1345                Python::attach(|py| {
1346                    let duration = Duration::days(days) + Duration::seconds(seconds) + Duration::microseconds(microseconds);
1347
1348                    // Skip if outside Python's timedelta bounds
1349                    let max_seconds = 86_399_999_913_600;
1350                    if duration.whole_seconds() <= max_seconds && duration.whole_seconds() >= -max_seconds {
1351                        let py_delta = duration.into_pyobject(py).unwrap();
1352
1353                        // You could add FromPyObject for Duration to fully test the roundtrip
1354                        // For now we'll just check that the Python object has the expected properties
1355                        let total_seconds = py_delta.call_method0(intern!(py, "total_seconds")).unwrap().extract::<f64>().unwrap();
1356                        let expected_seconds = duration.whole_seconds() as f64 + (duration.subsec_nanoseconds() as f64 / 1_000_000_000.0);
1357
1358                        // Allow small floating point differences
1359                        assert_eq!(total_seconds, expected_seconds);
1360                    }
1361                })
1362            }
1363
1364            #[test]
1365            fn test_all_valid_dates(
1366                year in 1i32..=9999,
1367                month_num in 1u8..=12,
1368            ) {
1369                Python::attach(|py| {
1370                    let month = match month_num {
1371                        1 => (Month::January, 31),
1372                        2 => {
1373                            // Handle leap years
1374                            if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
1375                                (Month::February, 29)
1376                            } else {
1377                                (Month::February, 28)
1378                            }
1379                        },
1380                        3 => (Month::March, 31),
1381                        4 => (Month::April, 30),
1382                        5 => (Month::May, 31),
1383                        6 => (Month::June, 30),
1384                        7 => (Month::July, 31),
1385                        8 => (Month::August, 31),
1386                        9 => (Month::September, 30),
1387                        10 => (Month::October, 31),
1388                        11 => (Month::November, 30),
1389                        12 => (Month::December, 31),
1390                        _ => unreachable!(),
1391                    };
1392
1393                    // Test the entire month
1394                    for day in 1..=month.1 {
1395                        let date = Date::from_calendar_date(year, month.0, day).unwrap();
1396                        let py_date = date.into_pyobject(py).unwrap();
1397                        let roundtripped: Date = py_date.extract().unwrap();
1398                        assert_eq!(date, roundtripped);
1399                    }
1400                });
1401            }
1402
1403            #[test]
1404            fn test_time_time_roundtrip_random(
1405                hour in 0u8..=23u8,
1406                minute in 0u8..=59u8,
1407                second in 0u8..=59u8,
1408                microsecond in 0u32..=999999u32
1409            ) {
1410                Python::attach(|py| {
1411                    let time = Time::from_hms_micro(hour, minute, second, microsecond).unwrap();
1412                    let py_time = time.into_pyobject(py).unwrap();
1413                    let roundtripped: Time = py_time.extract().unwrap();
1414                    assert_eq!(time, roundtripped);
1415                });
1416            }
1417
1418            #[test]
1419            fn test_time_primitive_datetime_roundtrip_random(
1420                year in 1i32..=9999i32,
1421                month in 1u8..=12u8,
1422                day in 1u8..=28u8, // Use only valid days for all months
1423                hour in 0u8..=23u8,
1424                minute in 0u8..=59u8,
1425                second in 0u8..=59u8,
1426                microsecond in 0u32..=999999u32
1427            ) {
1428                Python::attach(|py| {
1429                    let month = match month {
1430                        1 => Month::January,
1431                        2 => Month::February,
1432                        3 => Month::March,
1433                        4 => Month::April,
1434                        5 => Month::May,
1435                        6 => Month::June,
1436                        7 => Month::July,
1437                        8 => Month::August,
1438                        9 => Month::September,
1439                        10 => Month::October,
1440                        11 => Month::November,
1441                        12 => Month::December,
1442                        _ => unreachable!(),
1443                    };
1444
1445                    let date = Date::from_calendar_date(year, month, day).unwrap();
1446                    let time = Time::from_hms_micro(hour, minute, second, microsecond).unwrap();
1447                    let dt = PrimitiveDateTime::new(date, time);
1448
1449                    let py_dt = dt.into_pyobject(py).unwrap();
1450                    let roundtripped: PrimitiveDateTime = py_dt.extract().unwrap();
1451                    assert_eq!(dt, roundtripped);
1452                });
1453            }
1454
1455            #[test]
1456            fn test_time_utc_offset_roundtrip_random(
1457                hours in -23i8..=23i8,
1458                minutes in -59i8..=59i8
1459            ) {
1460                // Skip invalid combinations where hour and minute signs don't match
1461                if (hours < 0 && minutes > 0) || (hours > 0 && minutes < 0) {
1462                    return Ok(());
1463                }
1464
1465                Python::attach(|py| {
1466                    if let Ok(offset) = UtcOffset::from_hms(hours, minutes, 0) {
1467                        let py_tz = offset.into_pyobject(py).unwrap();
1468                        let roundtripped: UtcOffset = py_tz.extract().unwrap();
1469                        assert_eq!(roundtripped.whole_hours(), hours);
1470                        assert_eq!(roundtripped.minutes_past_hour(), minutes);
1471                    }
1472                });
1473            }
1474
1475            #[test]
1476            fn test_time_offset_datetime_roundtrip_random(
1477                year in 1i32..=9999i32,
1478                month in 1u8..=12u8,
1479                day in 1u8..=28u8, // Use only valid days for all months
1480                hour in 0u8..=23u8,
1481                minute in 0u8..=59u8,
1482                second in 0u8..=59u8,
1483                microsecond in 0u32..=999999u32,
1484                tz_hour in -23i8..=23i8,
1485                tz_minute in 0i8..=59i8
1486            ) {
1487                Python::attach(|py| {
1488                    let month = match month {
1489                        1 => Month::January,
1490                        2 => Month::February,
1491                        3 => Month::March,
1492                        4 => Month::April,
1493                        5 => Month::May,
1494                        6 => Month::June,
1495                        7 => Month::July,
1496                        8 => Month::August,
1497                        9 => Month::September,
1498                        10 => Month::October,
1499                        11 => Month::November,
1500                        12 => Month::December,
1501                        _ => unreachable!(),
1502                    };
1503
1504                    let date = Date::from_calendar_date(year, month, day).unwrap();
1505                    let time = Time::from_hms_micro(hour, minute, second, microsecond).unwrap();
1506
1507                    // Handle timezone sign correctly
1508                    let tz_minute = if tz_hour < 0 { -tz_minute } else { tz_minute };
1509
1510                    if let Ok(offset) = UtcOffset::from_hms(tz_hour, tz_minute, 0) {
1511                        let dt = PrimitiveDateTime::new(date, time).assume_offset(offset);
1512                        let py_dt = dt.into_pyobject(py).unwrap();
1513                        let roundtripped: OffsetDateTime = py_dt.extract().unwrap();
1514
1515                        assert_eq!(dt.year(), roundtripped.year());
1516                        assert_eq!(dt.month(), roundtripped.month());
1517                        assert_eq!(dt.day(), roundtripped.day());
1518                        assert_eq!(dt.hour(), roundtripped.hour());
1519                        assert_eq!(dt.minute(), roundtripped.minute());
1520                        assert_eq!(dt.second(), roundtripped.second());
1521                        assert_eq!(dt.microsecond(), roundtripped.microsecond());
1522                        assert_eq!(dt.offset().whole_hours(), roundtripped.offset().whole_hours());
1523                        assert_eq!(dt.offset().minutes_past_hour(), roundtripped.offset().minutes_past_hour());
1524                    }
1525                });
1526            }
1527
1528            #[test]
1529            fn test_time_utc_datetime_roundtrip_random(
1530                year in 1i32..=9999i32,
1531                month in 1u8..=12u8,
1532                day in 1u8..=28u8, // Use only valid days for all months
1533                hour in 0u8..=23u8,
1534                minute in 0u8..=59u8,
1535                second in 0u8..=59u8,
1536                microsecond in 0u32..=999999u32
1537            ) {
1538                Python::attach(|py| {
1539                    let month = match month {
1540                        1 => Month::January,
1541                        2 => Month::February,
1542                        3 => Month::March,
1543                        4 => Month::April,
1544                        5 => Month::May,
1545                        6 => Month::June,
1546                        7 => Month::July,
1547                        8 => Month::August,
1548                        9 => Month::September,
1549                        10 => Month::October,
1550                        11 => Month::November,
1551                        12 => Month::December,
1552                        _ => unreachable!(),
1553                    };
1554
1555                    let date = Date::from_calendar_date(year, month, day).unwrap();
1556                    let time = Time::from_hms_micro(hour, minute, second, microsecond).unwrap();
1557                    let primitive_dt = PrimitiveDateTime::new(date, time);
1558                    let dt: UtcDateTime = primitive_dt.assume_utc().into();
1559
1560                    let py_dt = dt.into_pyobject(py).unwrap();
1561                    let roundtripped: UtcDateTime = py_dt.extract().unwrap();
1562
1563                    assert_eq!(dt.year(), roundtripped.year());
1564                    assert_eq!(dt.month(), roundtripped.month());
1565                    assert_eq!(dt.day(), roundtripped.day());
1566                    assert_eq!(dt.hour(), roundtripped.hour());
1567                    assert_eq!(dt.minute(), roundtripped.minute());
1568                    assert_eq!(dt.second(), roundtripped.second());
1569                    assert_eq!(dt.microsecond(), roundtripped.microsecond());
1570                })
1571            }
1572        }
1573    }
1574}
⚠️ Internal Docs ⚠️ Not Public API 👉 Official Docs Here