@@ -392,8 +392,12 @@ def get_precision_timestamp(time: str) -> PrecisionTimestamp:
392392 except ValueError :
393393 return PrecisionTimestamp .SECONDS
394394
395- # Split the fraction from the timezone specifier ('Z' or 'z')
396- s_fraction , _ = s_fraction .split ("Z" ) if "Z" in s_fraction else s_fraction .split ("z" )
395+ # Split the fraction from the timezone specifier ('Z' or 'z') when present
396+ if "Z" in s_fraction :
397+ s_fraction , _ = s_fraction .split ("Z" , 1 )
398+ elif "z" in s_fraction :
399+ s_fraction , _ = s_fraction .split ("z" , 1 )
400+ # else: no timezone suffix (e.g. "000"), use fraction as-is
397401
398402 # If the fraction is more than 6 digits long, it's a nanosecond timestamp
399403 if len (s_fraction ) > 6 :
@@ -407,28 +411,33 @@ def timestamp_conversion(timestamp: str | dict | _typing.Any) -> _dt.datetime:
407411 Converts a timestamp-like value to a timezone-aware UTC datetime.
408412
409413 Accepts RFC 3339/ISO 8601 strings or Firebase Timestamp objects
410- (with 'seconds', 'nanoseconds' attributes).
414+ (with 'seconds' and 'nanoseconds' or 'nanos ' attributes).
411415 """
412416 # Handle Firebase Timestamp object case
413- # Accept dict-like objects, or python objects with 'seconds' and 'nanoseconds' attributes
414- if hasattr (timestamp , "seconds" ) and hasattr (timestamp , "nanoseconds" ):
415- # Normalize nanoseconds into seconds (handles values >= 1_000_000_000 or < 0)
416- carry , ns = divmod (int (timestamp .nanoseconds ), 1_000_000_000 )
417- secs = int (timestamp .seconds ) + carry
418- # Truncate (deterministic, no floating precision issues, matches string path behavior)
419- microseconds = ns // 1_000
420- # Build without using fromtimestamp
421- epoch = _dt .datetime (1970 , 1 , 1 , tzinfo = _dt .timezone .utc )
422- return epoch + _dt .timedelta (seconds = secs , microseconds = microseconds )
423- elif isinstance (timestamp , dict ) and "seconds" in timestamp and "nanoseconds" in timestamp :
424- # Normalize nanoseconds into seconds (handles values >= 1_000_000_000 or < 0)
425- carry , ns = divmod (int (timestamp ["nanoseconds" ]), 1_000_000_000 )
426- secs = int (timestamp ["seconds" ]) + carry
427- # Truncate (deterministic, no floating precision issues, matches string path behavior)
428- microseconds = ns // 1_000
429- # Build without using fromtimestamp
430- epoch = _dt .datetime (1970 , 1 , 1 , tzinfo = _dt .timezone .utc )
431- return epoch + _dt .timedelta (seconds = secs , microseconds = microseconds )
417+ # Accept objects with .seconds and .nanoseconds or .nanos
418+ if hasattr (timestamp , "seconds" ):
419+ ns_val = getattr (timestamp , "nanoseconds" , getattr (timestamp , "nanos" , None ))
420+ if ns_val is not None :
421+ # Normalize nanoseconds into seconds (handles values >= 1_000_000_000 or < 0)
422+ carry , ns = divmod (int (ns_val ), 1_000_000_000 )
423+ secs = int (timestamp .seconds ) + carry
424+ # Truncate (deterministic, no floating precision issues, matches string path behavior)
425+ microseconds = ns // 1_000
426+ # Build without using fromtimestamp
427+ epoch = _dt .datetime (1970 , 1 , 1 , tzinfo = _dt .timezone .utc )
428+ return epoch + _dt .timedelta (seconds = secs , microseconds = microseconds )
429+ # Handle dict (e.g. protobuf-style with "nanos" or "nanoseconds")
430+ elif isinstance (timestamp , dict ) and "seconds" in timestamp :
431+ nanos_val = timestamp .get ("nanoseconds" , timestamp .get ("nanos" ))
432+ if nanos_val is not None :
433+ # Normalize nanoseconds into seconds (handles values >= 1_000_000_000 or < 0)
434+ carry , ns = divmod (int (nanos_val ), 1_000_000_000 )
435+ secs = int (timestamp ["seconds" ]) + carry
436+ # Truncate (deterministic, no floating precision issues, matches string path behavior)
437+ microseconds = ns // 1_000
438+ # Build without using fromtimestamp
439+ epoch = _dt .datetime (1970 , 1 , 1 , tzinfo = _dt .timezone .utc )
440+ return epoch + _dt .timedelta (seconds = secs , microseconds = microseconds )
432441
433442 # Assume string input
434443 if not isinstance (timestamp , str ):
0 commit comments