Skip to content

Commit cbadc1a

Browse files
committed
fix: timestamp parsing for RTDB events
1 parent f0cb25f commit cbadc1a

4 files changed

Lines changed: 97 additions & 11 deletions

File tree

src/firebase_functions/db_fn.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
# pylint: disable=protected-access
1919
import dataclasses as _dataclass
20-
import datetime as _dt
2120
import functools as _functools
2221
import typing as _typing
2322

@@ -126,10 +125,7 @@ def _db_endpoint_handler(
126125
id=event_attributes["id"],
127126
source=event_attributes["source"],
128127
type=event_attributes["type"],
129-
time=_dt.datetime.strptime(
130-
event_attributes["time"],
131-
"%Y-%m-%dT%H:%M:%S.%f%z",
132-
),
128+
time=_util.timestamp_conversion(event_attributes["time"]),
133129
data=database_event_data,
134130
subject=event_attributes["subject"],
135131
params=params,

src/firebase_functions/private/util.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -386,14 +386,15 @@ def __str__(self) -> str:
386386

387387
def get_precision_timestamp(time: str) -> PrecisionTimestamp:
388388
"""Return a bool which indicates if the timestamp is in nanoseconds"""
389-
# Split the string into date-time and fraction of second
390-
try:
391-
_, s_fraction = time.split(".")
392-
except ValueError:
389+
if "." not in time:
393390
return PrecisionTimestamp.SECONDS
394391

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")
392+
_, s_fraction = time.split(".", 1)
393+
fraction_match = _re.match(r"\d+", s_fraction)
394+
if fraction_match is None:
395+
raise ValueError("Invalid timestamp")
396+
397+
s_fraction = fraction_match.group()
397398

398399
# If the fraction is more than 6 digits long, it's a nanosecond timestamp
399400
if len(s_fraction) > 6:

tests/test_db.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Tests for the db module.
33
"""
44

5+
import datetime as dt
56
import unittest
67
from unittest import mock
78

@@ -81,3 +82,37 @@ def test_missing_auth_context(self):
8182
self.assertIsNotNone(event_arg)
8283
self.assertEqual(event_arg.auth_type, "unknown")
8384
self.assertIsNone(event_arg.auth_id)
85+
86+
def test_written_event_parses_timestamp_without_microseconds(self):
87+
func = mock.Mock(__name__="example_func_no_microseconds")
88+
decorated_func = db_fn.on_value_written(reference="/items/{itemId}")(func)
89+
90+
event = CloudEvent(
91+
attributes={
92+
"specversion": "1.0",
93+
"id": "issue-257-repro",
94+
"source": "//firebase.test/projects/demo-test/instances/my-instance/refs/items/123",
95+
"subject": "refs/items/123",
96+
"type": "google.firebase.database.ref.v1.written",
97+
"time": "2025-10-30T21:15:51Z",
98+
"instance": "my-instance",
99+
"ref": "/items/123",
100+
"firebasedatabasehost": "my-instance.firebaseio.com",
101+
"location": "location",
102+
},
103+
data={
104+
"data": {"existing": True},
105+
"delta": {"updated": True},
106+
},
107+
)
108+
109+
decorated_func(event)
110+
111+
func.assert_called_once()
112+
event_arg = func.call_args.args[0]
113+
self.assertEqual(
114+
event_arg.time,
115+
dt.datetime(2025, 10, 30, 21, 15, 51, tzinfo=dt.timezone.utc),
116+
)
117+
self.assertEqual(event_arg.data.after, {"existing": True, "updated": True})
118+
self.assertEqual(event_arg.params, {"itemId": "123"})

tests/test_util.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
nanoseconds_timestamp_conversion,
2929
normalize_path,
3030
second_timestamp_conversion,
31+
timestamp_conversion,
3132
)
3233

3334
test_bucket = "python-functions-testing.appspot.com"
@@ -107,6 +108,59 @@ def test_second_conversion():
107108
assert second_timestamp_conversion(input_timestamp) == expected_datetime
108109

109110

111+
def test_timestamp_conversion_supported_formats():
112+
"""
113+
Testing shared timestamp conversion handles supported RTDB and CloudEvent formats.
114+
"""
115+
timestamps = [
116+
(
117+
"2024-04-10T12:00:00.000Z",
118+
_dt.datetime(2024, 4, 10, 12, 0, tzinfo=_dt.timezone.utc),
119+
),
120+
(
121+
"2024-04-10T12:00:00.123456Z",
122+
_dt.datetime(2024, 4, 10, 12, 0, 0, 123456, tzinfo=_dt.timezone.utc),
123+
),
124+
(
125+
"2024-04-10T12:00:00.123456+05:30",
126+
_dt.datetime(
127+
2024,
128+
4,
129+
10,
130+
12,
131+
0,
132+
0,
133+
123456,
134+
tzinfo=_dt.timezone(_dt.timedelta(hours=5, minutes=30)),
135+
),
136+
),
137+
(
138+
"2024-04-10T12:00:00.123456-0700",
139+
_dt.datetime(
140+
2024,
141+
4,
142+
10,
143+
12,
144+
0,
145+
0,
146+
123456,
147+
tzinfo=_dt.timezone(-_dt.timedelta(hours=7)),
148+
),
149+
),
150+
(
151+
"2023-01-01T12:34:56.123456789Z",
152+
_dt.datetime(2023, 1, 1, 12, 34, 56, 123456, tzinfo=_dt.timezone.utc),
153+
),
154+
(
155+
"2025-10-30T21:15:51Z",
156+
_dt.datetime(2025, 10, 30, 21, 15, 51, tzinfo=_dt.timezone.utc),
157+
),
158+
]
159+
160+
for input_timestamp, expected_datetime in timestamps:
161+
assert timestamp_conversion(input_timestamp) == expected_datetime
162+
163+
110164
def test_is_nanoseconds_timestamp():
111165
"""
112166
Testing is_nanoseconds_timestamp works as intended

0 commit comments

Comments
 (0)