1- use chrono:: { self , DateTime , FixedOffset , NaiveDate , NaiveDateTime , NaiveTime } ;
1+ use chrono:: { self , DateTime , FixedOffset , NaiveDate , NaiveDateTime , NaiveTime , TimeZone } ;
2+ use chrono_tz:: Tz ;
23use geo_types:: { coord, Coord , Line as LineSegment , LineString , Point , Rect } ;
34use itertools:: Itertools ;
45use macaddr:: { MacAddr6 , MacAddr8 } ;
@@ -626,8 +627,7 @@ impl ToSql for PythonDTO {
626627#[ allow( clippy:: needless_pass_by_value) ]
627628pub fn convert_parameters ( parameters : Py < PyAny > ) -> RustPSQLDriverPyResult < Vec < PythonDTO > > {
628629 let mut result_vec: Vec < PythonDTO > = vec ! [ ] ;
629-
630- result_vec = Python :: with_gil ( |gil| {
630+ Python :: with_gil ( |gil| {
631631 let params = parameters. extract :: < Vec < Py < PyAny > > > ( gil) . map_err ( |_| {
632632 RustPSQLDriverError :: PyToRustValueConversionError (
633633 "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
637637 for parameter in params {
638638 result_vec. push ( py_to_rust ( parameter. bind ( gil) ) ?) ;
639639 }
640- Ok :: < Vec < PythonDTO > , RustPSQLDriverError > ( result_vec )
640+ Ok :: < ( ) , RustPSQLDriverError > ( ( ) )
641641 } ) ?;
642+
642643 Ok ( result_vec)
643644}
644645
@@ -744,6 +745,81 @@ pub fn py_sequence_into_postgres_array(
744745 }
745746}
746747
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+
747823/// Convert single python parameter to `PythonDTO` enum.
748824///
749825/// # Errors
@@ -849,6 +925,11 @@ pub fn py_to_rust(parameter: &pyo3::Bound<'_, PyAny>) -> RustPSQLDriverPyResult<
849925 return Ok ( PythonDTO :: PyDateTime ( pydatetime_no_tz) ) ;
850926 }
851927
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+
852933 return Err ( RustPSQLDriverError :: PyToRustValueConversionError (
853934 "Can not convert you datetime to rust type" . into ( ) ,
854935 ) ) ;
0 commit comments