Skip to main content

pyo3/conversions/
time.rs

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