44from uuid import UUID
55
66import numpy as np
7+ import pandas as pd
78from google .protobuf .descriptor import FieldDescriptor
89from google .protobuf .duration_pb2 import Duration
910from google .protobuf .message import Message
1718from tilebox .datasets .datasets .v1 .well_known_types_pb2 import Geometry , LatLon , LatLonAlt , Quaternion , Vec3
1819
1920ScalarProtoFieldValue = Message | float | str | bool | bytes
21+
22+
23+ def _is_missing (value : Any ) -> bool :
24+ """Check if a value represents a missing/null value.
25+
26+ Handles None, np.nan, pd.NA, NaT, and other pandas missing value sentinels.
27+ This is needed for pandas 3.0+ compatibility where object-dtype columns use
28+ np.nan instead of None for missing values.
29+ """
30+ if value is None :
31+ return True
32+ try :
33+ return bool (pd .isna (value ))
34+ except (TypeError , ValueError ):
35+ return False
2036ProtoFieldValue = ScalarProtoFieldValue | Sequence [ScalarProtoFieldValue ] | None
2137
2238_FILL_VALUES_BY_DTYPE : dict [type [np .dtype [Any ]], Any ] = {
@@ -107,7 +123,7 @@ def from_proto(self, value: ProtoFieldValue) -> int:
107123 return value .seconds * 10 ** 9 + value .nanos
108124
109125 def to_proto (self , value : DatetimeScalar ) -> Timestamp | None :
110- if value is None or (isinstance (value , np .datetime64 ) and np .isnat (value )):
126+ if _is_missing ( value ) or (isinstance (value , np .datetime64 ) and np .isnat (value )):
111127 return None
112128 # we use pandas to_datetime function to handle a variety of input types that can be coerced to datetimes
113129 seconds , nanos = divmod (to_datetime (value , utc = True ).value , 10 ** 9 )
@@ -124,7 +140,7 @@ def from_proto(self, value: ProtoFieldValue) -> int:
124140 return value .seconds * 10 ** 9 + value .nanos
125141
126142 def to_proto (self , value : str | float | timedelta | np .timedelta64 ) -> Duration | None :
127- if value is None or (isinstance (value , np .timedelta64 ) and np .isnat (value )):
143+ if _is_missing ( value ) or (isinstance (value , np .timedelta64 ) and np .isnat (value )):
128144 return None
129145 # we use pandas to_timedelta function to handle a variety of input types that can be coerced to timedeltas
130146 seconds , nanos = divmod (to_timedelta (value ).value , 10 ** 9 ) # type: ignore[arg-type]
@@ -141,7 +157,7 @@ def from_proto(self, value: ProtoFieldValue) -> str:
141157 return str (UUID (bytes = value .uuid ))
142158
143159 def to_proto (self , value : str | UUID ) -> UUIDMessage | None :
144- if not value : # None or empty string
160+ if _is_missing ( value ) or value == "" : # missing or empty string
145161 return None
146162
147163 if isinstance (value , str ):
@@ -160,7 +176,7 @@ def from_proto(self, value: ProtoFieldValue) -> Any:
160176 return from_wkb (value .wkb )
161177
162178 def to_proto (self , value : Any ) -> Geometry | None :
163- if value is None :
179+ if _is_missing ( value ) :
164180 return None
165181 return Geometry (wkb = value .wkb )
166182
@@ -175,7 +191,7 @@ def from_proto(self, value: ProtoFieldValue) -> tuple[float, float, float]:
175191 return value .x , value .y , value .z
176192
177193 def to_proto (self , value : tuple [float , float , float ]) -> Vec3 | None :
178- if value is None or np .all (np .isnan (value )):
194+ if _is_missing ( value ) or np .all (np .isnan (value )):
179195 return None
180196 return Vec3 (x = value [0 ], y = value [1 ], z = value [2 ])
181197
@@ -190,7 +206,7 @@ def from_proto(self, value: ProtoFieldValue) -> tuple[float, float, float, float
190206 return value .q1 , value .q2 , value .q3 , value .q4
191207
192208 def to_proto (self , value : tuple [float , float , float , float ]) -> Quaternion | None :
193- if value is None or np .all (np .isnan (value )):
209+ if _is_missing ( value ) or np .all (np .isnan (value )):
194210 return None
195211 return Quaternion (q1 = value [0 ], q2 = value [1 ], q3 = value [2 ], q4 = value [3 ])
196212
@@ -205,7 +221,7 @@ def from_proto(self, value: ProtoFieldValue) -> tuple[float, float]:
205221 return value .latitude , value .longitude
206222
207223 def to_proto (self , value : tuple [float , float ]) -> LatLon | None :
208- if value is None or np .all (np .isnan (value )):
224+ if _is_missing ( value ) or np .all (np .isnan (value )):
209225 return None
210226 return LatLon (latitude = value [0 ], longitude = value [1 ])
211227
@@ -221,7 +237,7 @@ def from_proto(self, value: ProtoFieldValue) -> tuple[float, float, float]:
221237 return value .latitude , value .longitude , value .altitude
222238
223239 def to_proto (self , value : tuple [float , float , float ]) -> LatLonAlt | None :
224- if value is None or np .all (np .isnan (value )):
240+ if _is_missing ( value ) or np .all (np .isnan (value )):
225241 return None
226242 return LatLonAlt (latitude = value [0 ], longitude = value [1 ], altitude = value [2 ])
227243
0 commit comments