Skip to content

Commit 12ce27a

Browse files
fix: conversion to handle nanosecond precision timestamp (#109)
1 parent 31ad203 commit 12ce27a

File tree

3 files changed

+109
-6
lines changed

3 files changed

+109
-6
lines changed

src/firebase_functions/firestore_fn.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import dataclasses as _dataclass
1919
import functools as _functools
2020
import typing as _typing
21-
import datetime as _dt
2221
import google.events.cloud.firestore as _firestore
2322
import google.cloud.firestore_v1 as _firestore_v1
2423
import firebase_functions.private.util as _util
@@ -111,10 +110,14 @@ def _firestore_endpoint_handler(
111110
event_namespace = event_attributes["namespace"]
112111
event_document = event_attributes["document"]
113112
event_database = event_attributes["database"]
114-
event_time = _dt.datetime.strptime(
115-
event_attributes["time"],
116-
"%Y-%m-%dT%H:%M:%S.%f%z",
117-
)
113+
114+
time = event_attributes["time"]
115+
is_nanoseconds = _util.is_precision_timestamp(time)
116+
117+
if is_nanoseconds:
118+
event_time = _util.nanoseconds_timestamp_conversion(time)
119+
else:
120+
event_time = _util.microsecond_timestamp_conversion(time)
118121

119122
if _DEFAULT_APP_NAME not in _apps:
120123
initialize_app()

src/firebase_functions/private/util.py

+37
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import json as _json
2020
import typing as _typing
2121
import dataclasses as _dataclasses
22+
import datetime as _dt
2223
import enum as _enum
2324
from flask import Request as _Request
2425
from functions_framework import logging as _logging
@@ -310,6 +311,42 @@ def firebase_config() -> None | FirebaseConfig:
310311
return FirebaseConfig(storage_bucket=json_data.get("storageBucket"))
311312

312313

314+
def nanoseconds_timestamp_conversion(time: str) -> _dt.datetime:
315+
"""Converts a nanosecond timestamp and returns a datetime object of the current time in UTC"""
316+
317+
# Separate the date and time part from the nanoseconds.
318+
datetime_str, nanosecond_str = time.replace("Z", "").replace("z",
319+
"").split(".")
320+
# Parse the date and time part of the string.
321+
event_time = _dt.datetime.strptime(datetime_str, "%Y-%m-%dT%H:%M:%S")
322+
# Add the microseconds and timezone.
323+
event_time = event_time.replace(microsecond=int(nanosecond_str[:6]),
324+
tzinfo=_dt.timezone.utc)
325+
326+
return event_time
327+
328+
329+
def is_precision_timestamp(time: str) -> bool:
330+
"""Return a bool which indicates if the timestamp is in nanoseconds"""
331+
# Split the string into date-time and fraction of second
332+
_, s_fraction = time.split(".")
333+
334+
# Split the fraction from the timezone specifier ('Z' or 'z')
335+
s_fraction, _ = s_fraction.split(
336+
"Z") if "Z" in s_fraction else s_fraction.split("z")
337+
338+
# If the fraction is more than 6 digits long, it's a nanosecond timestamp
339+
return len(s_fraction) > 6
340+
341+
342+
def microsecond_timestamp_conversion(time: str) -> _dt.datetime:
343+
"""Converts a microsecond timestamp and returns a datetime object of the current time in UTC"""
344+
return _dt.datetime.strptime(
345+
time,
346+
"%Y-%m-%dT%H:%M:%S.%f%z",
347+
)
348+
349+
313350
def normalize_path(path: str) -> str:
314351
"""
315352
Normalize a path string to a consistent format.

tests/test_util.py

+64-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
Internal utils tests.
1616
"""
1717
from os import environ, path
18-
from firebase_functions.private.util import firebase_config, normalize_path
18+
from firebase_functions.private.util import firebase_config, microsecond_timestamp_conversion, nanoseconds_timestamp_conversion, is_precision_timestamp, normalize_path
19+
import datetime as _dt
1920

2021
test_bucket = "python-functions-testing.appspot.com"
2122
test_config_file = path.join(path.dirname(path.realpath(__file__)),
@@ -42,6 +43,68 @@ def test_firebase_config_loads_from_env_file():
4243
"Failure, firebase_config did not load from env variable.")
4344

4445

46+
def test_microsecond_conversion():
47+
"""
48+
Testing microsecond_timestamp_conversion works as intended
49+
"""
50+
timestamps = [
51+
("2023-06-20T10:15:22.396358Z", "2023-06-20T10:15:22.396358Z"),
52+
("2021-02-20T11:23:45.987123Z", "2021-02-20T11:23:45.987123Z"),
53+
("2022-09-18T09:15:38.246824Z", "2022-09-18T09:15:38.246824Z"),
54+
("2010-09-18T09:15:38.246824Z", "2010-09-18T09:15:38.246824Z"),
55+
]
56+
57+
for input_timestamp, expected_output in timestamps:
58+
expected_datetime = _dt.datetime.strptime(expected_output,
59+
"%Y-%m-%dT%H:%M:%S.%fZ")
60+
expected_datetime = expected_datetime.replace(tzinfo=_dt.timezone.utc)
61+
assert microsecond_timestamp_conversion(
62+
input_timestamp) == expected_datetime
63+
64+
65+
def test_nanosecond_conversion():
66+
"""
67+
Testing nanoseconds_timestamp_conversion works as intended
68+
"""
69+
timestamps = [
70+
("2023-01-01T12:34:56.123456789Z", "2023-01-01T12:34:56.123456Z"),
71+
("2023-02-14T14:37:52.987654321Z", "2023-02-14T14:37:52.987654Z"),
72+
("2023-03-21T06:43:58.564738291Z", "2023-03-21T06:43:58.564738Z"),
73+
("2023-08-15T22:22:22.222222222Z", "2023-08-15T22:22:22.222222Z"),
74+
]
75+
76+
for input_timestamp, expected_output in timestamps:
77+
expected_datetime = _dt.datetime.strptime(expected_output,
78+
"%Y-%m-%dT%H:%M:%S.%fZ")
79+
expected_datetime = expected_datetime.replace(tzinfo=_dt.timezone.utc)
80+
assert nanoseconds_timestamp_conversion(
81+
input_timestamp) == expected_datetime
82+
83+
84+
def test_is_nanoseconds_timestamp():
85+
"""
86+
Testing is_nanoseconds_timestamp works as intended
87+
"""
88+
microsecond_timestamp1 = "2023-06-20T10:15:22.396358Z"
89+
microsecond_timestamp2 = "2021-02-20T11:23:45.987123Z"
90+
microsecond_timestamp3 = "2022-09-18T09:15:38.246824Z"
91+
microsecond_timestamp4 = "2010-09-18T09:15:38.246824Z"
92+
93+
nanosecond_timestamp1 = "2023-01-01T12:34:56.123456789Z"
94+
nanosecond_timestamp2 = "2023-02-14T14:37:52.987654321Z"
95+
nanosecond_timestamp3 = "2023-03-21T06:43:58.564738291Z"
96+
nanosecond_timestamp4 = "2023-08-15T22:22:22.222222222Z"
97+
98+
assert is_precision_timestamp(microsecond_timestamp1) is False
99+
assert is_precision_timestamp(microsecond_timestamp2) is False
100+
assert is_precision_timestamp(microsecond_timestamp3) is False
101+
assert is_precision_timestamp(microsecond_timestamp4) is False
102+
assert is_precision_timestamp(nanosecond_timestamp1) is True
103+
assert is_precision_timestamp(nanosecond_timestamp2) is True
104+
assert is_precision_timestamp(nanosecond_timestamp3) is True
105+
assert is_precision_timestamp(nanosecond_timestamp4) is True
106+
107+
45108
def test_normalize_document_path():
46109
"""
47110
Testing "document" path passed to Firestore event listener

0 commit comments

Comments
 (0)