Skip to content

Commit ed64ce7

Browse files
committed
Handle optional fractional seconds
1 parent fbb8771 commit ed64ce7

File tree

4 files changed

+79
-43
lines changed

4 files changed

+79
-43
lines changed

src/firebase_functions/firestore_fn.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,7 @@ def _firestore_endpoint_handler(
112112
event_database = event_attributes["database"]
113113

114114
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)
115+
event_time = _util.timestamp_conversion(time)
121116

122117
if _DEFAULT_APP_NAME not in _apps:
123118
initialize_app()

src/firebase_functions/private/_alerts_fn.py

+7-24
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import typing as _typing
1818
import datetime as _dt
1919
import cloudevents.http as _ce
20+
import util as _util
2021
from firebase_functions.alerts import FirebaseAlertData
2122

2223
from functions_framework import logging as _logging
@@ -105,10 +106,7 @@ def regression_alert_payload_from_ce_payload(payload: dict):
105106
return RegressionAlertPayload(
106107
type=payload["type"],
107108
issue=issue_from_ce_payload(payload["issue"]),
108-
resolve_time=_dt.datetime.strptime(
109-
payload["resolveTime"],
110-
"%Y-%m-%dT%H:%M:%S.%f%z",
111-
),
109+
resolve_time= _util.timestamp_conversion(payload["resolveTime"])
112110
)
113111

114112

@@ -125,10 +123,7 @@ def trending_issue_details_from_ce_payload(payload: dict):
125123
def stability_digest_payload_from_ce_payload(payload: dict):
126124
from firebase_functions.alerts.crashlytics_fn import StabilityDigestPayload
127125
return StabilityDigestPayload(
128-
digest_date=_dt.datetime.strptime(
129-
payload["digestDate"],
130-
"%Y-%m-%dT%H:%M:%S.%f%z",
131-
),
126+
digest_date=_util.timestamp_conversion(payload["digestDate"]),
132127
trending_issues=[
133128
trending_issue_details_from_ce_payload(issue)
134129
for issue in payload["trendingIssues"]
@@ -139,10 +134,7 @@ def velocity_alert_payload_from_ce_payload(payload: dict):
139134
from firebase_functions.alerts.crashlytics_fn import VelocityAlertPayload
140135
return VelocityAlertPayload(
141136
issue=issue_from_ce_payload(payload["issue"]),
142-
create_time=_dt.datetime.strptime(
143-
payload["createTime"],
144-
"%Y-%m-%dT%H:%M:%S.%f%z",
145-
),
137+
create_time=_util.timestamp_conversion(payload["createTime"]),
146138
crash_count=payload["crashCount"],
147139
crash_percentage=payload["crashPercentage"],
148140
first_version=payload["firstVersion"],
@@ -186,14 +178,8 @@ def firebase_alert_data_from_ce(event_dict: dict,) -> FirebaseAlertData:
186178
_logging.warning(f"Unhandled Firebase Alerts alert type: {alert_type}")
187179

188180
return FirebaseAlertData(
189-
create_time=_dt.datetime.strptime(
190-
event_dict["createTime"],
191-
"%Y-%m-%dT%H:%M:%S.%f%z",
192-
),
193-
end_time=_dt.datetime.strptime(
194-
event_dict["endTime"],
195-
"%Y-%m-%dT%H:%M:%S.%f%z",
196-
) if "endTime" in event_dict else None,
181+
create_time=_util.timestamp_conversion(event_dict["createTime"]),
182+
end_time=_util.timestamp_conversion(event_dict["endTime"]) if "endTime" in event_dict else None,
197183
payload=alert_payload,
198184
)
199185

@@ -217,10 +203,7 @@ def event_from_ce_helper(raw: _ce.CloudEvent, cls, app_id=True):
217203
"subject":
218204
event_dict["subject"] if "subject" in event_dict else None,
219205
"time":
220-
_dt.datetime.strptime(
221-
event_dict["time"],
222-
"%Y-%m-%dT%H:%M:%S.%f%z",
223-
),
206+
_util.timestamp_conversion(event_dict["time"]),
224207
"type":
225208
event_dict["type"],
226209
}

src/firebase_functions/private/util.py

+35-3
Original file line numberDiff line numberDiff line change
@@ -336,22 +336,54 @@ def nanoseconds_timestamp_conversion(time: str) -> _dt.datetime:
336336

337337
return event_time
338338

339+
def second_timestamp_conversion(time: str) -> _dt.datetime:
340+
"""Converts a second timestamp and returns a datetime object of the current time in UTC"""
341+
return _dt.datetime.strptime(
342+
time,
343+
"%Y-%m-%dT%H:%M:%S%z",
344+
)
345+
346+
class PrecisionTimestamp(_enum.Enum):
347+
"""
348+
The status of a token.
349+
"""
339350

340-
def is_precision_timestamp(time: str) -> bool:
351+
NANOSECONDS = "NANOSECONDS"
352+
353+
MICROSECONDS = "MICROSECONDS"
354+
355+
SECONDS = "SECONDS"
356+
357+
358+
def get_precision_timestamp(time: str) -> PrecisionTimestamp:
341359
"""Return a bool which indicates if the timestamp is in nanoseconds"""
342360
# Split the string into date-time and fraction of second
343361
try:
344362
_, s_fraction = time.split(".")
345363
except ValueError:
346-
return False # If there's no decimal, it's not a nanosecond timestamp.
364+
return PrecisionTimestamp.SECONDS
347365

348366
# Split the fraction from the timezone specifier ('Z' or 'z')
349367
s_fraction, _ = s_fraction.split(
350368
"Z") if "Z" in s_fraction else s_fraction.split("z")
351369

352370
# If the fraction is more than 6 digits long, it's a nanosecond timestamp
353-
return len(s_fraction) > 6
371+
if len(s_fraction) > 6:
372+
return PrecisionTimestamp.NANOSECONDS
373+
else:
374+
return PrecisionTimestamp.MICROSECONDS
375+
376+
377+
def timestamp_conversion(time: str) -> _dt.datetime:
378+
"""Converts a timestamp and returns a datetime object of the current time in UTC"""
379+
precision_timestamp = get_precision_timestamp(time)
354380

381+
if precision_timestamp == PrecisionTimestamp.NANOSECONDS:
382+
return nanoseconds_timestamp_conversion(time)
383+
elif precision_timestamp == PrecisionTimestamp.MICROSECONDS:
384+
return microsecond_timestamp_conversion(time)
385+
elif precision_timestamp == PrecisionTimestamp.SECONDS:
386+
return second_timestamp_conversion(time)
355387

356388
def microsecond_timestamp_conversion(time: str) -> _dt.datetime:
357389
"""Converts a microsecond timestamp and returns a datetime object of the current time in UTC"""

tests/test_util.py

+36-10
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
Internal utils tests.
1616
"""
1717
from os import environ, path
18-
from firebase_functions.private.util import firebase_config, microsecond_timestamp_conversion, nanoseconds_timestamp_conversion, is_precision_timestamp, normalize_path, deep_merge
18+
from firebase_functions.private.util import firebase_config, microsecond_timestamp_conversion, nanoseconds_timestamp_conversion, get_precision_timestamp, normalize_path, deep_merge, PrecisionTimestamp, second_timestamp_conversion
1919
import datetime as _dt
2020

2121
test_bucket = "python-functions-testing.appspot.com"
@@ -80,6 +80,23 @@ def test_nanosecond_conversion():
8080
assert nanoseconds_timestamp_conversion(
8181
input_timestamp) == expected_datetime
8282

83+
def test_second_conversion():
84+
"""
85+
Testing seconds_timestamp_conversion works as intended
86+
"""
87+
timestamps = [
88+
("2023-01-01T12:34:56Z", "2023-01-01T12:34:56Z"),
89+
("2023-02-14T14:37:52Z", "2023-02-14T14:37:52Z"),
90+
("2023-03-21T06:43:58Z", "2023-03-21T06:43:58Z"),
91+
("2023-10-06T07:00:00Z", "2023-10-06T07:00:00Z"),
92+
]
93+
94+
for input_timestamp, expected_output in timestamps:
95+
expected_datetime = _dt.datetime.strptime(expected_output,
96+
"%Y-%m-%dT%H:%M:%SZ")
97+
expected_datetime = expected_datetime.replace(tzinfo=_dt.timezone.utc)
98+
assert second_timestamp_conversion(
99+
input_timestamp) == expected_datetime
83100

84101
def test_is_nanoseconds_timestamp():
85102
"""
@@ -95,19 +112,28 @@ def test_is_nanoseconds_timestamp():
95112
nanosecond_timestamp3 = "2023-03-21T06:43:58.564738291Z"
96113
nanosecond_timestamp4 = "2023-08-15T22:22:22.222222222Z"
97114

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
115+
second_timestamp1 = "2023-01-01T12:34:56Z"
116+
second_timestamp2 = "2023-02-14T14:37:52Z"
117+
second_timestamp3 = "2023-03-21T06:43:58Z"
118+
second_timestamp4 = "2023-08-15T22:22:22Z"
119+
120+
assert get_precision_timestamp(microsecond_timestamp1) is PrecisionTimestamp.MICROSECONDS
121+
assert get_precision_timestamp(microsecond_timestamp2) is PrecisionTimestamp.MICROSECONDS
122+
assert get_precision_timestamp(microsecond_timestamp3) is PrecisionTimestamp.MICROSECONDS
123+
assert get_precision_timestamp(microsecond_timestamp4) is PrecisionTimestamp.MICROSECONDS
124+
assert get_precision_timestamp(nanosecond_timestamp1) is PrecisionTimestamp.NANOSECONDS
125+
assert get_precision_timestamp(nanosecond_timestamp2) is PrecisionTimestamp.NANOSECONDS
126+
assert get_precision_timestamp(nanosecond_timestamp3) is PrecisionTimestamp.NANOSECONDS
127+
assert get_precision_timestamp(nanosecond_timestamp4) is PrecisionTimestamp.NANOSECONDS
128+
assert get_precision_timestamp(second_timestamp1) is PrecisionTimestamp.SECONDS
129+
assert get_precision_timestamp(second_timestamp2) is PrecisionTimestamp.SECONDS
130+
assert get_precision_timestamp(second_timestamp3) is PrecisionTimestamp.SECONDS
131+
assert get_precision_timestamp(second_timestamp4) is PrecisionTimestamp.SECONDS
106132

107133

108134
def test_normalize_document_path():
109135
"""
110-
Testing "document" path passed to Firestore event listener
136+
Testing "document" path passed to Firestore event listener
111137
is normalized.
112138
"""
113139
test_path = "/test/document/"

0 commit comments

Comments
 (0)