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