cftime_rs/
py_bindings.rs

1use crate::calendars::Calendar;
2use crate::datetime::CFDatetime;
3use crate::duration::CFDuration;
4use crate::encoder::CFEncoder;
5use crate::{constants, decoder::*};
6use pyo3::exceptions::PyValueError;
7use pyo3::prelude::*;
8use pyo3::types::PyDateTime;
9use std::str::FromStr;
10use std::sync::Arc;
11#[pyclass]
12#[derive(Clone)]
13pub struct PyCFCalendar {
14    pub calendar: Calendar,
15}
16
17/// PyCFDuration is a wrapper around Rust CFDuration
18/// All the methods depends on the Calendar definitions found in
19/// [udunits package](https://github.com/nco/nco/blob/master/data/udunits.dat)
20///
21/// This duration can be added to a PyCFDatetime
22/// The result of the substraction between a PyCFDatetime and a PyCFDatetime gives a PyCFDuration
23#[pyclass]
24pub struct PyCFDuration {
25    pub duration: CFDuration,
26}
27
28#[pymethods]
29impl PyCFCalendar {
30    #[staticmethod]
31    pub fn from_str(s: String) -> PyResult<Self> {
32        let calendar = Calendar::from_str(s.as_str())
33            .map_err(|e| PyValueError::new_err(format!("Could not parse calendar: {}", e)))?;
34        Ok(Self { calendar })
35    }
36}
37
38#[pymethods]
39impl PyCFDuration {
40    /// Makes a new `PyCFDuration` with given number of seconds, nanoseconds and specific calendar.
41    #[new]
42    pub fn new(seconds: i64, nanoseconds: i64, calendar: PyCFCalendar) -> Self {
43        Self {
44            duration: CFDuration::new(seconds, nanoseconds, calendar.calendar),
45        }
46    }
47    /// Makes a new `PyCFDuration` with given number of years and specific calendar.
48    #[staticmethod]
49    pub fn from_years(years: i64, calendar: PyCFCalendar) -> PyCFDuration {
50        Self {
51            duration: CFDuration::from_years(years, calendar.calendar),
52        }
53    }
54    /// Makes a new `PyCFDuration` with given number of months and specific calendar.
55    #[staticmethod]
56    pub fn from_months(months: i64, calendar: PyCFCalendar) -> PyCFDuration {
57        Self {
58            duration: CFDuration::from_months(months, calendar.calendar),
59        }
60    }
61    /// Makes a new `PyCFDuration` with given number of weeks and specific calendar.
62    #[staticmethod]
63    pub fn from_weeks(weeks: i64, calendar: PyCFCalendar) -> PyCFDuration {
64        Self {
65            duration: CFDuration::from_weeks(weeks, calendar.calendar),
66        }
67    }
68    /// Makes a new `PyCFDuration` with given number of days and specific calendar.
69    #[staticmethod]
70    pub fn from_days(days: i64, calendar: PyCFCalendar) -> PyCFDuration {
71        Self {
72            duration: CFDuration::from_days(days, calendar.calendar),
73        }
74    }
75    /// Makes a new `PyCFDuration` with given number of hours and specific calendar.
76    #[staticmethod]
77    pub fn from_hours(hours: i64, calendar: PyCFCalendar) -> PyCFDuration {
78        Self {
79            duration: CFDuration::from_hours(hours, calendar.calendar),
80        }
81    }
82    /// Makes a new `PyCFDuration` with given number of minutes and specific calendar.
83    #[staticmethod]
84    pub fn from_minutes(minutes: i64, calendar: PyCFCalendar) -> PyCFDuration {
85        Self {
86            duration: CFDuration::from_minutes(minutes, calendar.calendar),
87        }
88    }
89    /// Makes a new `PyCFDuration` with given number of seconds and specific calendar.
90    #[staticmethod]
91    pub fn from_seconds(seconds: i64, calendar: PyCFCalendar) -> PyCFDuration {
92        Self {
93            duration: CFDuration::from_seconds(seconds, calendar.calendar),
94        }
95    }
96    /// Makes a new `PyCFDuration` with given number of milliseconds and specific calendar.
97    #[staticmethod]
98    pub fn from_milliseconds(milliseconds: i64, calendar: PyCFCalendar) -> PyCFDuration {
99        Self {
100            duration: CFDuration::from_milliseconds(milliseconds, calendar.calendar),
101        }
102    }
103    /// Makes a new `PyCFDuration` with given number of microseconds and specific calendar.
104    #[staticmethod]
105    pub fn from_microseconds(microseconds: i64, calendar: PyCFCalendar) -> PyCFDuration {
106        Self {
107            duration: CFDuration::from_microseconds(microseconds, calendar.calendar),
108        }
109    }
110    /// Makes a new `PyCFDuration` with given number of nanoseconds and specific calendar.
111    #[staticmethod]
112    pub fn from_nanoseconds(nanoseconds: i64, calendar: PyCFCalendar) -> PyCFDuration {
113        Self {
114            duration: CFDuration::from_nanoseconds(nanoseconds, calendar.calendar),
115        }
116    }
117    /// Returns the total number of years in the duration.
118    pub fn num_years(&self) -> f64 {
119        self.duration.num_years()
120    }
121    /// Returns the total number of months in the duration.
122    pub fn num_months(&self) -> f64 {
123        self.duration.num_months()
124    }
125    /// Returns the total number of weeks in the duration.
126    pub fn num_weeks(&self) -> f64 {
127        self.duration.num_weeks()
128    }
129    /// Returns the total number of days in the duration.
130    pub fn num_days(&self) -> f64 {
131        self.duration.num_days()
132    }
133    /// Returns the total number of hours in the duration.
134    pub fn num_hours(&self) -> f64 {
135        self.duration.num_hours()
136    }
137    /// Returns the total number of minutes in the duration.
138    pub fn num_minutes(&self) -> f64 {
139        self.duration.num_minutes()
140    }
141    /// Returns the total number of seconds in the duration.
142    pub fn num_seconds(&self) -> f64 {
143        self.duration.num_seconds()
144    }
145    /// Returns the total number of milliseconds in the duration.
146    pub fn num_milliseconds(&self) -> f64 {
147        self.duration.num_milliseconds()
148    }
149    /// Returns the total number of microseconds in the duration.
150    pub fn num_microseconds(&self) -> f64 {
151        self.duration.num_microseconds()
152    }
153    /// Returns the total number of nanoseconds in the duration.
154    pub fn num_nanoseconds(&self) -> f64 {
155        self.duration.num_nanoseconds()
156    }
157    /// Returns an ISO 8601 formatted string.
158    pub fn __repr__(&self) -> String {
159        format!("{}", self.duration)
160    }
161    /// Returns an ISO 8601 formatted string.
162    pub fn __str__(&self) -> String {
163        self.duration.to_string()
164    }
165
166    pub fn __sub__(&self, other: &PyCFDuration) -> PyResult<PyCFDuration> {
167        Ok(PyCFDuration {
168            duration: (&self.duration - &other.duration)
169                .map_err(|e| PyValueError::new_err(format!("{}", e)))?,
170        })
171    }
172
173    pub fn __add__(&self, other: &PyCFDuration) -> PyResult<PyCFDuration> {
174        Ok(PyCFDuration {
175            duration: (&self.duration + &other.duration)
176                .map_err(|e| PyValueError::new_err(format!("{}", e)))?,
177        })
178    }
179
180    pub fn __neg__(&self) -> PyCFDuration {
181        let duration = -&self.duration;
182        PyCFDuration { duration: duration }
183    }
184}
185
186/// PyCFDatetime is a wrapper around Rust CFDatetime
187/// It represents a date in a specific calendar
188/// All the methods depends on the Calendar definitions found in
189/// [udunits package](https://github.com/nco/nco/blob/master/data/udunits.dat)
190#[pyclass]
191#[derive(Clone, PartialEq)]
192pub struct PyCFDatetime {
193    pub dt: Arc<CFDatetime>,
194}
195
196#[pymethods]
197impl PyCFDatetime {
198    /// Makes a new `PyCFDatetime` with given year, month, day, hour, minute, second and specific calendar
199    #[new]
200    pub fn new(
201        year: i64,
202        month: u8,
203        day: u8,
204        hour: u8,
205        minute: u8,
206        second: f32,
207        calendar: PyCFCalendar,
208    ) -> PyResult<Self> {
209        let dt =
210            CFDatetime::from_ymd_hms(year, month, day, hour, minute, second, calendar.calendar)
211                .map_err(|e| PyValueError::new_err(e.to_string()))?;
212        Ok(Self { dt: dt.into() })
213    }
214    /// Returns the year, month and day of the date.
215    pub fn ymd(&self) -> PyResult<(i64, u8, u8)> {
216        let (year, month, day, _, _, _) = self
217            .ymd_hms()
218            .map_err(|e| PyValueError::new_err(e.to_string()))?;
219        Ok((year, month, day))
220    }
221    /// Returns the hour, minute and second of the date.
222    pub fn hms(&self) -> PyResult<(u8, u8, u8)> {
223        let (_, _, _, hour, min, sec) = self
224            .ymd_hms()
225            .map_err(|e| PyValueError::new_err(e.to_string()))?;
226        Ok((hour, min, sec))
227    }
228    /// Returns the year, month, day, hour, minute, second of the date.
229    pub fn ymd_hms(&self) -> PyResult<(i64, u8, u8, u8, u8, u8)> {
230        self.dt
231            .ymd_hms()
232            .map_err(|e| PyValueError::new_err(e.to_string()))
233    }
234    /// Makes a new `PyCFDatetime` with given year, month, day, hour, minute, second and specific calendar
235    #[staticmethod]
236    pub fn from_ymd_hms(
237        year: i64,
238        month: u8,
239        day: u8,
240        hour: u8,
241        minute: u8,
242        second: f32,
243        calendar: PyCFCalendar,
244    ) -> PyResult<Self> {
245        let dt =
246            CFDatetime::from_ymd_hms(year, month, day, hour, minute, second, calendar.calendar)
247                .map_err(|e| PyValueError::new_err(e.to_string()))?;
248        Ok(Self { dt: dt.into() })
249    }
250    /// Makes a new `PyCFDatetime` with given hour, minute, second and specific calendar.
251    /// The year, month, day are set to 1970-01-01
252    #[staticmethod]
253    pub fn from_hms(hour: u8, minute: u8, second: f32, calendar: PyCFCalendar) -> PyResult<Self> {
254        let dt = CFDatetime::from_ymd_hms(
255            constants::UNIX_DEFAULT_YEAR,
256            constants::UNIX_DEFAULT_MONTH,
257            constants::UNIX_DEFAULT_DAY,
258            hour,
259            minute,
260            second,
261            calendar.calendar,
262        )
263        .map_err(|e| PyValueError::new_err(e.to_string()))?;
264        Ok(Self { dt: dt.into() })
265    }
266    /// Makes a new `PyCFDatetime` with given year, month, day and specific calendar.
267    /// The hour, minute, second are set to 0
268    #[staticmethod]
269    pub fn from_ymd(year: i64, month: u8, day: u8, calendar: PyCFCalendar) -> PyResult<Self> {
270        let dt = CFDatetime::from_ymd_hms(year, month, day, 0, 0, 0.0, calendar.calendar)
271            .map_err(|e| PyValueError::new_err(e.to_string()))?;
272        Ok(Self { dt: dt.into() })
273    }
274    /// Makes a new `PyCFDatetime` with given timestamp, nanoseconds and specific calendar.
275    #[staticmethod]
276    pub fn from_timestamp(
277        timestamp: i64,
278        nanoseconds: u32,
279        calendar: PyCFCalendar,
280    ) -> PyResult<Self> {
281        let dt = CFDatetime::from_timestamp(timestamp, nanoseconds, calendar.calendar)
282            .map_err(|e| PyValueError::new_err(e.to_string()))?;
283        Ok(Self { dt: dt.into() })
284    }
285    /// Returns the hours of the date.
286    pub fn hours(&self) -> PyResult<u8> {
287        let (hour, _, _) = self.hms()?;
288        Ok(hour)
289    }
290    /// Returns the minutes of the date.
291    pub fn minutes(&self) -> PyResult<u8> {
292        let (_, min, _) = self.hms()?;
293        Ok(min)
294    }
295    /// Returns the seconds of the date.
296    pub fn seconds(&self) -> PyResult<u8> {
297        let (_, _, sec) = self.hms()?;
298        Ok(sec)
299    }
300    /// Returns the nanoseconds of the date.
301    pub fn nanoseconds(&self) -> u32 {
302        self.dt.nanoseconds()
303    }
304    /// Change the calendar of the PyCFDateTime.
305    ///
306    /// # Arguments
307    ///
308    /// * `calendar` - The new calendar to be applied.
309    ///
310    /// # Returns
311    ///
312    /// A new instance of `Self` with the updated calendar.
313    pub fn change_calendar(&self, calendar: PyCFCalendar) -> PyResult<Self> {
314        let new_dt = self
315            .dt
316            .change_calendar(calendar.calendar)
317            .map_err(|e| PyValueError::new_err(e.to_string()))?;
318        Ok(Self { dt: new_dt.into() })
319    }
320
321    /// Changes the calendar of the DateTime based on the internal timestamp.
322    ///
323    /// # Arguments
324    ///
325    /// * `calendar` - The new calendar to use.
326    ///
327    /// # Returns
328    ///
329    /// A new PyCFDateTime object.
330    ///
331    /// # Errors
332    ///
333    /// Returns a PyValueError if an error occurs while changing the calendar.
334    pub fn change_calendar_from_timestamp(&self, calendar: PyCFCalendar) -> PyResult<Self> {
335        // Call the change_calendar_from_timestamp method on self.dt
336        let new_dt = self
337            .dt
338            .change_calendar_from_timestamp(calendar.calendar)
339            .map_err(|e| PyValueError::new_err(e.to_string()))?;
340
341        // Create a new DateTime object with the updated dt value
342        Ok(Self { dt: new_dt.into() })
343    }
344
345    fn to_pydatetime<'a>(&self, py: Python<'a>) -> PyResult<&'a PyDateTime> {
346        let (year, month, day, hour, minute, second) = self
347            .ymd_hms()
348            .map_err(|e| PyValueError::new_err(format!("Could not convert to datetime: {}", e)))?;
349        let nanoseconds = self.nanoseconds();
350        let microsecond = nanoseconds / 1_000;
351        PyDateTime::new(
352            py,
353            year as i32,
354            month,
355            day,
356            hour,
357            minute,
358            second,
359            microsecond as u32,
360            None,
361        )
362    }
363    fn to_pydatetime_from_timestamp<'a>(&self, py: Python<'a>) -> PyResult<&'a PyDateTime> {
364        PyDateTime::from_timestamp(
365            py,
366            self.dt.timestamp() as f64 + self.dt.nanoseconds() as f64 / 1e9,
367            None,
368        )
369    }
370    fn __repr__(&self) -> String {
371        format!("PyCFDatetime({}, {})", self.dt, self.dt.calendar())
372    }
373    fn __str__(&self) -> String {
374        self.dt.to_string()
375    }
376    fn __sub__(&self, other: &PyCFDatetime) -> PyResult<PyCFDuration> {
377        let duration =
378            (&*self.dt - &*other.dt).map_err(|e| PyValueError::new_err(e.to_string()))?;
379        Ok(PyCFDuration { duration: duration })
380    }
381    fn __add__(&self, other: &PyCFDuration) -> PyResult<PyCFDatetime> {
382        let dt = (&*self.dt + &other.duration).map_err(|e| PyValueError::new_err(e.to_string()))?;
383        Ok(PyCFDatetime { dt: dt.into() })
384    }
385    fn __eq__(&self, other: &PyCFDatetime) -> PyResult<bool> {
386        Ok(self.dt == other.dt)
387    }
388}
389
390impl std::fmt::Display for PyCFDatetime {
391    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
392        self.dt.fmt(f)
393    }
394}
395
396macro_rules! decode_numbers {
397    ($numbers:expr, $units:expr, $calendar:expr, $($t:ty),+) => {
398        {
399            $(
400                if let Ok(numbers) = $numbers.extract::<Vec<$t>>() {
401                    numbers.decode_cf($units.as_str(), $calendar)
402                        .map_err(|e| PyValueError::new_err(format!("Could not decode numbers {} into PyCFDatetime: {}", $numbers, e)))?
403                } else
404            )+
405            {
406                let supported_types = stringify!($($t),+);
407                return Err(PyValueError::new_err(format!(
408                "Could not convert array to supported types. \
409                Needs an one-dimensional array of one the following types: {}",supported_types)))
410            }
411        }
412    };
413}
414
415#[pyfunction]
416fn num2date(numbers: &PyAny, units: String, calendar: String) -> PyResult<Vec<PyCFDatetime>> {
417    let calendar = Calendar::from_str(calendar.as_str())
418        .map_err(|e| PyValueError::new_err(format!("Could not parse calendar: {}", e)))?;
419    // The order is important always prefer a bigger representation such as i64 or f64 to a
420    // smaller representation such as i32 or f32
421    // This is because if we convert some number coming from python we can downcast from f64 to f32
422    // and lose precision
423    let datetimes = decode_numbers!(numbers, units, calendar, i64, i32, f64, f32);
424    Ok(datetimes
425        .into_iter()
426        .map(|dt| PyCFDatetime { dt: dt.into() })
427        .collect())
428}
429
430#[pyfunction]
431#[pyo3(signature = (numbers, units, calendar, from_timestamp=false))]
432fn num2pydate<'a>(
433    py: Python<'a>,
434    numbers: &'a PyAny,
435    units: String,
436    calendar: String,
437    from_timestamp: Option<bool>,
438) -> PyResult<Vec<&'a PyDateTime>> {
439    match from_timestamp {
440        Some(true) => num2date(numbers, units, calendar)?
441            .iter()
442            .map(|dt| dt.to_pydatetime_from_timestamp(py))
443            .collect::<Result<Vec<_>, _>>(),
444        _ => num2date(numbers, units, calendar)?
445            .iter()
446            .map(|dt| dt.to_pydatetime(py))
447            .collect::<Result<Vec<_>, _>>(),
448    }
449}
450enum DType {
451    Int32,
452    Int64,
453    Float32,
454    Float64,
455    Unknown,
456}
457
458const INT_32_TYPES: &[&str] = &["i32"];
459const INT_64_TYPES: &[&str] = &["i64", "i", "integer", "int"];
460const FLOAT_32_TYPES: &[&str] = &["f32"];
461const FLOAT_64_TYPES: &[&str] = &["f64", "f", "float"];
462
463impl FromStr for DType {
464    type Err = String;
465    fn from_str(s: &str) -> Result<Self, Self::Err> {
466        match s.to_lowercase().as_str() {
467            s if INT_32_TYPES.iter().any(|&x| x == s) => Ok(DType::Int32),
468            s if INT_64_TYPES.iter().any(|&x| x == s) => Ok(DType::Int64),
469            s if FLOAT_32_TYPES.iter().any(|&x| x == s) => Ok(DType::Float32),
470            s if FLOAT_64_TYPES.iter().any(|&x| x == s) => Ok(DType::Float64),
471            _ => Ok(DType::Unknown),
472        }
473    }
474}
475
476#[pyfunction]
477fn date2num(
478    py: Python,
479    datetimes: Vec<PyCFDatetime>,
480    units: String,
481    calendar: String,
482    dtype: String,
483) -> PyResult<PyObject> {
484    let calendar = Calendar::from_str(calendar.as_str())
485        .map_err(|e| PyValueError::new_err(format!("Could not parse calendar: {}", e)))?;
486    let dtype_enum = DType::from_str(dtype.as_str())
487        .map_err(|e| PyValueError::new_err(format!("Could not parse dtype: {}", e)))?;
488    let dts: Vec<&CFDatetime> = datetimes.iter().map(|pydatetime| &*pydatetime.dt).collect();
489    match dtype_enum {
490        DType::Int32 => {
491            let numbers: Vec<i32> = dts
492                .encode_cf(units.as_str(), calendar)
493                .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
494            Ok(numbers.into_py(py))
495        }
496        DType::Int64 => {
497            let numbers: Vec<i64> = dts
498                .encode_cf(units.as_str(), calendar)
499                .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
500            Ok(numbers.into_py(py))
501        }
502        DType::Float32 => {
503            let numbers: Vec<f32> = dts
504                .encode_cf(units.as_str(), calendar)
505                .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
506            Ok(numbers.into_py(py))
507        }
508        DType::Float64 => {
509            let numbers: Vec<f64> = dts
510                .encode_cf(units.as_str(), calendar)
511                .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
512            Ok(numbers.into_py(py))
513        }
514        DType::Unknown => Err(PyValueError::new_err(format!(
515            "Invalid dtype `{}`. For i32 use {}. For i64 use {}. For f32 use {}. For f64 use {}.",
516            dtype,
517            INT_32_TYPES.join(", "),
518            INT_64_TYPES.join(", "),
519            FLOAT_32_TYPES.join(", "),
520            FLOAT_64_TYPES.join(", ")
521        ))),
522    }
523}
524// Create a newtype wrapper for Vec<PyDateTime>
525
526pub struct PyDateTimeList<'a> {
527    datetimes: Vec<&'a PyDateTime>,
528}
529
530impl<'a> pyo3::FromPyObject<'a> for PyDateTimeList<'a> {
531    fn extract(obj: &'a PyAny) -> pyo3::PyResult<Self> {
532        let py_list = obj.downcast::<pyo3::types::PyList>()?;
533        let mut datetimes = Vec::with_capacity(py_list.len());
534        for elem in py_list {
535            let py_dt = elem.extract::<&PyDateTime>()?;
536            datetimes.push(py_dt);
537        }
538        Ok(PyDateTimeList {
539            datetimes: datetimes,
540        })
541    }
542}
543
544#[pyfunction]
545fn pydate2num(
546    py: Python,
547    datetimes: PyDateTimeList,
548    units: String,
549    calendar: String,
550    dtype: String,
551) -> PyResult<PyObject> {
552    let calendar = Calendar::from_str(calendar.as_str())
553        .map_err(|e| PyValueError::new_err(format!("Could not parse calendar: {}", e)))?;
554    let dtype_enum = DType::from_str(dtype.as_str())
555        .map_err(|e| PyValueError::new_err(format!("Could not parse dtype: {}", e)))?;
556    let mut dts: Vec<CFDatetime> = Vec::with_capacity(datetimes.datetimes.len());
557
558    for pydt in datetimes.datetimes.iter() {
559        let year = pydt.getattr("year")?.extract::<i64>()?;
560        let month = pydt.getattr("month")?.extract::<u8>()?;
561        let day = pydt.getattr("day")?.extract::<u8>()?;
562        let hour = pydt.getattr("hour")?.extract::<u8>()?;
563        let minute = pydt.getattr("minute")?.extract::<u8>()?;
564        let second = pydt.getattr("second")?.extract::<u8>()?;
565        let microsecond = pydt.getattr("microsecond")?.extract::<u32>()?;
566        let new_second = second as f32 + (microsecond / 1_000_000) as f32;
567        dts.push(
568            CFDatetime::from_ymd_hms(year, month, day, hour, minute, new_second, calendar)
569                .map_err(|e| {
570                    PyValueError::new_err(format!(
571                        "Could not convert datetime to CFDatetime: {}",
572                        e
573                    ))
574                })?,
575        );
576    }
577
578    match dtype_enum {
579        DType::Int32 => {
580            let numbers: Vec<i32> = dts
581                .encode_cf(units.as_str(), calendar)
582                .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
583            Ok(numbers.into_py(py))
584        }
585        DType::Int64 => {
586            let numbers: Vec<i64> = dts
587                .encode_cf(units.as_str(), calendar)
588                .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
589            Ok(numbers.into_py(py))
590        }
591        DType::Float32 => {
592            let numbers: Vec<f32> = dts
593                .encode_cf(units.as_str(), calendar)
594                .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
595            Ok(numbers.into_py(py))
596        }
597        DType::Float64 => {
598            let numbers: Vec<f64> = dts
599                .encode_cf(units.as_str(), calendar)
600                .map_err(|e| PyValueError::new_err(format!("Could not encode datetimes: {}", e)))?;
601            Ok(numbers.into_py(py))
602        }
603        DType::Unknown => Err(PyValueError::new_err(format!(
604            "Invalid dtype `{}`. For i32 use {}. For i64 use {}. For f32 use {}. For f64 use {}.",
605            dtype,
606            INT_32_TYPES.join(", "),
607            INT_64_TYPES.join(", "),
608            FLOAT_32_TYPES.join(", "),
609            FLOAT_64_TYPES.join(", ")
610        ))),
611    }
612}
613
614/// cftime_rs is a python module that is implemented in Rust.
615#[pymodule]
616fn cftime_rs(_py: Python, m: &PyModule) -> PyResult<()> {
617    m.add_function(wrap_pyfunction!(num2date, m)?)?;
618    m.add_function(wrap_pyfunction!(date2num, m)?)?;
619    m.add_function(wrap_pyfunction!(num2pydate, m)?)?;
620    m.add_function(wrap_pyfunction!(pydate2num, m)?)?;
621    m.add_class::<PyCFCalendar>()?;
622    m.add_class::<PyCFDuration>()?;
623    m.add_class::<PyCFDatetime>()?;
624
625    Ok(())
626}