1919import typing
2020
2121import pyarrow
22+ import whenever
2223
2324T = typing .TypeVar ("T" , pyarrow .Schema , pyarrow .Table )
2425
@@ -67,6 +68,89 @@ def make_nullable(value: T) -> T:
6768 raise TypeError (f"Unsupported type: { type (value )} " )
6869
6970
71+ def scalar_to_py_smart (value : pyarrow .Scalar ) -> typing .Any :
72+ """
73+ Convert a PyArrow Scalar to a Python object.
74+
75+ Unlike `pyarrow.Scalar.as_py()`, this function aims to properly handle
76+ times, timestamps, and intervals.
77+ """
78+
79+ if value is None or not value .is_valid :
80+ return None
81+ elif isinstance (value , (pyarrow .Time32Scalar , pyarrow .Time64Scalar )):
82+ # Needed because to_pylist uses the wrong object for times, losing precision...
83+ match value .type .unit :
84+ case "s" :
85+ return NaiveTime (
86+ hour = value .value // 3600 ,
87+ minute = (value .value % 3600 ) // 60 ,
88+ second = value .value % 60 ,
89+ nanosecond = 0 ,
90+ )
91+ case "ms" :
92+ return NaiveTime (
93+ hour = value .value // 3600000 ,
94+ minute = (value .value % 3600000 ) // 60000 ,
95+ second = (value .value % 60000 ) // 1000 ,
96+ nanosecond = (value .value % 1000 ) * 1000000 ,
97+ )
98+ case "us" :
99+ return NaiveTime (
100+ hour = value .value // 3600000000 ,
101+ minute = (value .value % 3600000000 ) // 60000000 ,
102+ second = (value .value % 60000000 ) // 1000000 ,
103+ nanosecond = (value .value % 1000000 ) * 1000 ,
104+ )
105+ case "ns" :
106+ return NaiveTime (
107+ hour = value .value // 3600000000000 ,
108+ minute = (value .value % 3600000000000 ) // 60000000000 ,
109+ second = (value .value % 60000000000 ) // 1000000000 ,
110+ nanosecond = value .value % 1000000000 ,
111+ )
112+ case _:
113+ raise NotImplementedError (str (value ))
114+ elif isinstance (value , pyarrow .TimestampScalar ):
115+ match value .type .unit :
116+ case "s" :
117+ nanos = value .value * 1_000_000_000
118+ case "ms" :
119+ nanos = value .value * 1_000_000
120+ case "us" :
121+ nanos = value .value * 1000
122+ case "ns" :
123+ nanos = value .value
124+
125+ instant = whenever .Instant .from_timestamp_nanos (nanos )
126+ if value .type .tz is None or value .type .tz == "" :
127+ # A bit sketch
128+ naive = whenever .PlainDateTime .parse_common_iso (
129+ instant .format_common_iso ()[:- 1 ]
130+ )
131+ return str (naive )
132+ elif value .type .tz == "UTC" :
133+ return str (instant )
134+ elif value .type .tz [0 ] in ("+" , "-" ):
135+ negative = value .type .tz [0 ] == "-"
136+ hours , _ , minutes = value .type .tz [1 :].partition (":" )
137+ tz_offset = whenever .TimeDelta (hours = int (hours ), minutes = int (minutes ))
138+ if negative :
139+ tz_offset = - tz_offset
140+ offset = instant .to_fixed_offset (tz_offset )
141+ return str (offset )
142+ zoned = instant .to_tz (value .type .tz )
143+ return str (zoned )
144+ elif isinstance (value , pyarrow .MonthDayNanoIntervalScalar ):
145+ mdn = value .as_py ()
146+ if mdn is None :
147+ return None
148+ else :
149+ return f"{ mdn [0 ]} M{ mdn [1 ]} d{ mdn [2 ]} ns"
150+
151+ return value .as_py ()
152+
153+
70154def to_pylist (table : pyarrow .Table ) -> list [dict [str , typing .Any ]]:
71155 """Convert a PyArrow Table to a list of dictionaries."""
72156 rows = []
@@ -75,51 +159,8 @@ def to_pylist(table: pyarrow.Table) -> list[dict[str, typing.Any]]:
75159 for col_idx in range (table .num_columns ):
76160 value = table .column (col_idx )[row_idx ]
77161 col_name = table .schema [col_idx ].name
78- if value is None :
79- row [col_name ] = None
80- elif isinstance (value , (pyarrow .Time32Scalar , pyarrow .Time64Scalar )):
81- # Needed because to_pylist uses the wrong object for times, losing precision...
82- match value .type .unit :
83- case "s" :
84- row [col_name ] = NaiveTime (
85- hour = value .value // 3600 ,
86- minute = (value .value % 3600 ) // 60 ,
87- second = value .value % 60 ,
88- nanosecond = 0 ,
89- )
90- case "ms" :
91- row [col_name ] = NaiveTime (
92- hour = value .value // 3600000 ,
93- minute = (value .value % 3600000 ) // 60000 ,
94- second = (value .value % 60000 ) // 1000 ,
95- nanosecond = (value .value % 1000 ) * 1000000 ,
96- )
97- case "us" :
98- row [col_name ] = NaiveTime (
99- hour = value .value // 3600000000 ,
100- minute = (value .value % 3600000000 ) // 60000000 ,
101- second = (value .value % 60000000 ) // 1000000 ,
102- nanosecond = (value .value % 1000000 ) * 1000 ,
103- )
104- case "ns" :
105- row [col_name ] = NaiveTime (
106- hour = value .value // 3600000000000 ,
107- minute = (value .value % 3600000000000 ) // 60000000000 ,
108- second = (value .value % 60000000000 ) // 1000000000 ,
109- nanosecond = value .value % 1000000000 ,
110- )
111- case _:
112- raise NotImplementedError (str (value ))
113- elif isinstance (value , pyarrow .MonthDayNanoIntervalScalar ):
114- mdn = value .as_py ()
115- if mdn is None :
116- row [col_name ] = None
117- else :
118- row [col_name ] = f"{ mdn [0 ]} M{ mdn [1 ]} d{ mdn [2 ]} ns"
119- else :
120- row [col_name ] = value .as_py ()
162+ row [col_name ] = scalar_to_py_smart (value )
121163 rows .append (row )
122-
123164 return rows
124165
125166
0 commit comments