1
- use chrono:: { self , DateTime , FixedOffset , NaiveDate , NaiveDateTime , NaiveTime } ;
1
+ use chrono:: { self , DateTime , FixedOffset , NaiveDate , NaiveDateTime , NaiveTime , TimeZone } ;
2
+ use chrono_tz:: Tz ;
2
3
use geo_types:: { coord, Coord , Line as LineSegment , LineString , Point , Rect } ;
3
4
use itertools:: Itertools ;
4
5
use macaddr:: { MacAddr6 , MacAddr8 } ;
@@ -626,8 +627,7 @@ impl ToSql for PythonDTO {
626
627
#[ allow( clippy:: needless_pass_by_value) ]
627
628
pub fn convert_parameters ( parameters : Py < PyAny > ) -> RustPSQLDriverPyResult < Vec < PythonDTO > > {
628
629
let mut result_vec: Vec < PythonDTO > = vec ! [ ] ;
629
-
630
- result_vec = Python :: with_gil ( |gil| {
630
+ Python :: with_gil ( |gil| {
631
631
let params = parameters. extract :: < Vec < Py < PyAny > > > ( gil) . map_err ( |_| {
632
632
RustPSQLDriverError :: PyToRustValueConversionError (
633
633
"Cannot convert you parameters argument into Rust type, please use List/Tuple"
@@ -637,8 +637,9 @@ pub fn convert_parameters(parameters: Py<PyAny>) -> RustPSQLDriverPyResult<Vec<P
637
637
for parameter in params {
638
638
result_vec. push ( py_to_rust ( parameter. bind ( gil) ) ?) ;
639
639
}
640
- Ok :: < Vec < PythonDTO > , RustPSQLDriverError > ( result_vec )
640
+ Ok :: < ( ) , RustPSQLDriverError > ( ( ) )
641
641
} ) ?;
642
+
642
643
Ok ( result_vec)
643
644
}
644
645
@@ -744,6 +745,81 @@ pub fn py_sequence_into_postgres_array(
744
745
}
745
746
}
746
747
748
+ /// Extract a value from a Python object, raising an error if missing or invalid
749
+ ///
750
+ /// # Errors
751
+ /// This function will return `Err` in the following cases:
752
+ /// - The Python object does not have the specified attribute
753
+ /// - The attribute exists but cannot be extracted into the specified Rust type
754
+ fn extract_value_from_python_object_or_raise < ' py , T > (
755
+ parameter : & ' py pyo3:: Bound < ' _ , PyAny > ,
756
+ attr_name : & str ,
757
+ ) -> Result < T , RustPSQLDriverError >
758
+ where
759
+ T : FromPyObject < ' py > ,
760
+ {
761
+ parameter
762
+ . getattr ( attr_name)
763
+ . ok ( )
764
+ . and_then ( |attr| attr. extract :: < T > ( ) . ok ( ) )
765
+ . ok_or_else ( || {
766
+ RustPSQLDriverError :: PyToRustValueConversionError ( "Invalid attribute" . into ( ) )
767
+ } )
768
+ }
769
+
770
+ /// Extract a timezone-aware datetime from a Python object.
771
+ /// This function retrieves various datetime components (`year`, `month`, `day`, etc.)
772
+ /// from a Python object and constructs a `DateTime<FixedOffset>`
773
+ ///
774
+ /// # Errors
775
+ /// This function will return `Err` in the following cases:
776
+ /// - The Python object does not contain or support one or more required datetime attributes
777
+ /// - The retrieved values are invalid for constructing a date, time, or datetime (e.g., invalid month or day)
778
+ /// - The timezone information (`tzinfo`) is not available or cannot be parsed
779
+ /// - The resulting datetime is ambiguous or invalid (e.g., due to DST transitions)
780
+ fn extract_datetime_from_python_object_attrs (
781
+ parameter : & pyo3:: Bound < ' _ , PyAny > ,
782
+ ) -> Result < DateTime < FixedOffset > , RustPSQLDriverError > {
783
+ let year = extract_value_from_python_object_or_raise :: < i32 > ( parameter, "year" ) ?;
784
+ let month = extract_value_from_python_object_or_raise :: < u32 > ( parameter, "month" ) ?;
785
+ let day = extract_value_from_python_object_or_raise :: < u32 > ( parameter, "day" ) ?;
786
+ let hour = extract_value_from_python_object_or_raise :: < u32 > ( parameter, "hour" ) ?;
787
+ let minute = extract_value_from_python_object_or_raise :: < u32 > ( parameter, "minute" ) ?;
788
+ let second = extract_value_from_python_object_or_raise :: < u32 > ( parameter, "second" ) ?;
789
+ let microsecond = extract_value_from_python_object_or_raise :: < u32 > ( parameter, "microsecond" ) ?;
790
+
791
+ let date = NaiveDate :: from_ymd_opt ( year, month, day)
792
+ . ok_or_else ( || RustPSQLDriverError :: PyToRustValueConversionError ( "Invalid date" . into ( ) ) ) ?;
793
+ let time = NaiveTime :: from_hms_micro_opt ( hour, minute, second, microsecond)
794
+ . ok_or_else ( || RustPSQLDriverError :: PyToRustValueConversionError ( "Invalid time" . into ( ) ) ) ?;
795
+ let naive_datetime = NaiveDateTime :: new ( date, time) ;
796
+
797
+ let raw_timestamp_tz = parameter
798
+ . getattr ( "tzinfo" )
799
+ . ok ( )
800
+ . and_then ( |tzinfo| tzinfo. getattr ( "key" ) . ok ( ) )
801
+ . and_then ( |key| key. extract :: < String > ( ) . ok ( ) )
802
+ . ok_or_else ( || {
803
+ RustPSQLDriverError :: PyToRustValueConversionError ( "Invalid timezone info" . into ( ) )
804
+ } ) ?;
805
+
806
+ let fixed_offset_datetime = raw_timestamp_tz
807
+ . parse :: < Tz > ( )
808
+ . map_err ( |_| {
809
+ RustPSQLDriverError :: PyToRustValueConversionError ( "Failed to parse TZ" . into ( ) )
810
+ } ) ?
811
+ . from_local_datetime ( & naive_datetime)
812
+ . single ( )
813
+ . ok_or_else ( || {
814
+ RustPSQLDriverError :: PyToRustValueConversionError (
815
+ "Ambiguous or invalid datetime" . into ( ) ,
816
+ )
817
+ } ) ?
818
+ . fixed_offset ( ) ;
819
+
820
+ Ok ( fixed_offset_datetime)
821
+ }
822
+
747
823
/// Convert single python parameter to `PythonDTO` enum.
748
824
///
749
825
/// # Errors
@@ -849,6 +925,11 @@ pub fn py_to_rust(parameter: &pyo3::Bound<'_, PyAny>) -> RustPSQLDriverPyResult<
849
925
return Ok ( PythonDTO :: PyDateTime ( pydatetime_no_tz) ) ;
850
926
}
851
927
928
+ let timestamp_tz = extract_datetime_from_python_object_attrs ( parameter) ;
929
+ if let Ok ( pydatetime_tz) = timestamp_tz {
930
+ return Ok ( PythonDTO :: PyDateTimeTz ( pydatetime_tz) ) ;
931
+ }
932
+
852
933
return Err ( RustPSQLDriverError :: PyToRustValueConversionError (
853
934
"Can not convert you datetime to rust type" . into ( ) ,
854
935
) ) ;
0 commit comments