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