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