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,84 @@ 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+ /// # Type Parameters
751+ /// - `T`: The type to which the attribute's value will be converted. This type must implement the `FromPyObject` trait
752+ ///
753+ /// # Errors
754+ /// This function will return `Err` in the following cases:
755+ /// - The Python object does not have the specified attribute
756+ /// - The attribute exists but cannot be extracted into the specified Rust type
757+ fn extract_value_from_python_object_or_raise < ' py , T > (
758+ parameter : & ' py pyo3:: Bound < ' _ , PyAny > ,
759+ attr_name : & str ,
760+ ) -> Result < T , RustPSQLDriverError >
761+ where
762+ T : FromPyObject < ' py > ,
763+ {
764+ parameter
765+ . getattr ( attr_name)
766+ . ok ( )
767+ . and_then ( |attr| attr. extract :: < T > ( ) . ok ( ) )
768+ . ok_or_else ( || {
769+ RustPSQLDriverError :: PyToRustValueConversionError ( "Invalid attribute" . into ( ) )
770+ } )
771+ }
772+
773+ /// Extract a timezone-aware datetime from a Python object.
774+ /// This function retrieves various datetime components (`year`, `month`, `day`, etc.)
775+ /// from a Python object and constructs a `DateTime<FixedOffset>`
776+ ///
777+ /// # Errors
778+ /// This function will return `Err` in the following cases:
779+ /// - The Python object does not contain or support one or more required datetime attributes
780+ /// - The retrieved values are invalid for constructing a date, time, or datetime (e.g., invalid month or day)
781+ /// - The timezone information (`tzinfo`) is not available or cannot be parsed
782+ /// - The resulting datetime is ambiguous or invalid (e.g., due to DST transitions)
783+ fn extract_datetime_from_python_object_attrs (
784+ parameter : & pyo3:: Bound < ' _ , PyAny > ,
785+ ) -> Result < DateTime < FixedOffset > , RustPSQLDriverError > {
786+ let year = extract_value_from_python_object_or_raise :: < i32 > ( parameter, "year" ) ?;
787+ let month = extract_value_from_python_object_or_raise :: < u32 > ( parameter, "month" ) ?;
788+ let day = extract_value_from_python_object_or_raise :: < u32 > ( parameter, "day" ) ?;
789+ let hour = extract_value_from_python_object_or_raise :: < u32 > ( parameter, "hour" ) ?;
790+ let minute = extract_value_from_python_object_or_raise :: < u32 > ( parameter, "minute" ) ?;
791+ let second = extract_value_from_python_object_or_raise :: < u32 > ( parameter, "second" ) ?;
792+ let microsecond = extract_value_from_python_object_or_raise :: < u32 > ( parameter, "microsecond" ) ?;
793+
794+ let date = NaiveDate :: from_ymd_opt ( year, month, day)
795+ . ok_or_else ( || RustPSQLDriverError :: PyToRustValueConversionError ( "Invalid date" . into ( ) ) ) ?;
796+ let time = NaiveTime :: from_hms_micro_opt ( hour, minute, second, microsecond)
797+ . ok_or_else ( || RustPSQLDriverError :: PyToRustValueConversionError ( "Invalid time" . into ( ) ) ) ?;
798+ let naive_datetime = NaiveDateTime :: new ( date, time) ;
799+
800+ let raw_timestamp_tz = parameter
801+ . getattr ( "tzinfo" )
802+ . ok ( )
803+ . and_then ( |tzinfo| tzinfo. getattr ( "key" ) . ok ( ) )
804+ . and_then ( |key| key. extract :: < String > ( ) . ok ( ) )
805+ . ok_or_else ( || {
806+ RustPSQLDriverError :: PyToRustValueConversionError ( "Invalid timezone info" . into ( ) )
807+ } ) ?;
808+
809+ let fixed_offset_datetime = raw_timestamp_tz
810+ . parse :: < Tz > ( )
811+ . map_err ( |_| {
812+ RustPSQLDriverError :: PyToRustValueConversionError ( "Failed to parse TZ" . into ( ) )
813+ } ) ?
814+ . from_local_datetime ( & naive_datetime)
815+ . single ( )
816+ . ok_or_else ( || {
817+ RustPSQLDriverError :: PyToRustValueConversionError (
818+ "Ambiguous or invalid datetime" . into ( ) ,
819+ )
820+ } ) ?
821+ . fixed_offset ( ) ;
822+
823+ Ok ( fixed_offset_datetime)
824+ }
825+
747826/// Convert single python parameter to `PythonDTO` enum.
748827///
749828/// # Errors
@@ -849,6 +928,11 @@ pub fn py_to_rust(parameter: &pyo3::Bound<'_, PyAny>) -> RustPSQLDriverPyResult<
849928 return Ok ( PythonDTO :: PyDateTime ( pydatetime_no_tz) ) ;
850929 }
851930
931+ let timestamp_tz = extract_datetime_from_python_object_attrs ( parameter) ;
932+ if let Ok ( pydatetime_tz) = timestamp_tz {
933+ return Ok ( PythonDTO :: PyDateTimeTz ( pydatetime_tz) ) ;
934+ }
935+
852936 return Err ( RustPSQLDriverError :: PyToRustValueConversionError (
853937 "Can not convert you datetime to rust type" . into ( ) ,
854938 ) ) ;
0 commit comments