Skip to content

fix: handle timestamps with no fractional seconds #149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions src/firebase_functions/firestore_fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,7 @@ def _firestore_endpoint_handler(
event_database = event_attributes["database"]

time = event_attributes["time"]
is_nanoseconds = _util.is_precision_timestamp(time)

if is_nanoseconds:
event_time = _util.nanoseconds_timestamp_conversion(time)
else:
event_time = _util.microsecond_timestamp_conversion(time)
event_time = _util.timestamp_conversion(time)

if _DEFAULT_APP_NAME not in _apps:
initialize_app()
Expand Down
33 changes: 8 additions & 25 deletions src/firebase_functions/private/_alerts_fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

# pylint: disable=protected-access,cyclic-import
import typing as _typing
import datetime as _dt
import cloudevents.http as _ce
import util as _util
from firebase_functions.alerts import FirebaseAlertData

from functions_framework import logging as _logging
Expand Down Expand Up @@ -105,10 +105,7 @@ def regression_alert_payload_from_ce_payload(payload: dict):
return RegressionAlertPayload(
type=payload["type"],
issue=issue_from_ce_payload(payload["issue"]),
resolve_time=_dt.datetime.strptime(
payload["resolveTime"],
"%Y-%m-%dT%H:%M:%S.%f%z",
),
resolve_time= _util.timestamp_conversion(payload["resolveTime"])
)


Expand All @@ -125,10 +122,7 @@ def trending_issue_details_from_ce_payload(payload: dict):
def stability_digest_payload_from_ce_payload(payload: dict):
from firebase_functions.alerts.crashlytics_fn import StabilityDigestPayload
return StabilityDigestPayload(
digest_date=_dt.datetime.strptime(
payload["digestDate"],
"%Y-%m-%dT%H:%M:%S.%f%z",
),
digest_date=_util.timestamp_conversion(payload["digestDate"]),
trending_issues=[
trending_issue_details_from_ce_payload(issue)
for issue in payload["trendingIssues"]
Expand All @@ -139,10 +133,7 @@ def velocity_alert_payload_from_ce_payload(payload: dict):
from firebase_functions.alerts.crashlytics_fn import VelocityAlertPayload
return VelocityAlertPayload(
issue=issue_from_ce_payload(payload["issue"]),
create_time=_dt.datetime.strptime(
payload["createTime"],
"%Y-%m-%dT%H:%M:%S.%f%z",
),
create_time=_util.timestamp_conversion(payload["createTime"]),
crash_count=payload["crashCount"],
crash_percentage=payload["crashPercentage"],
first_version=payload["firstVersion"],
Expand Down Expand Up @@ -186,14 +177,9 @@ def firebase_alert_data_from_ce(event_dict: dict,) -> FirebaseAlertData:
_logging.warning(f"Unhandled Firebase Alerts alert type: {alert_type}")

return FirebaseAlertData(
create_time=_dt.datetime.strptime(
event_dict["createTime"],
"%Y-%m-%dT%H:%M:%S.%f%z",
),
end_time=_dt.datetime.strptime(
event_dict["endTime"],
"%Y-%m-%dT%H:%M:%S.%f%z",
) if "endTime" in event_dict else None,
create_time=_util.timestamp_conversion(event_dict["createTime"]),
end_time=_util.timestamp_conversion(event_dict["endTime"])
if "endTime" in event_dict else None,
payload=alert_payload,
)

Expand All @@ -217,10 +203,7 @@ def event_from_ce_helper(raw: _ce.CloudEvent, cls, app_id=True):
"subject":
event_dict["subject"] if "subject" in event_dict else None,
"time":
_dt.datetime.strptime(
event_dict["time"],
"%Y-%m-%dT%H:%M:%S.%f%z",
),
_util.timestamp_conversion(event_dict["time"]),
"type":
event_dict["type"],
}
Expand Down
38 changes: 35 additions & 3 deletions src/firebase_functions/private/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,22 +336,54 @@ def nanoseconds_timestamp_conversion(time: str) -> _dt.datetime:

return event_time

def second_timestamp_conversion(time: str) -> _dt.datetime:
"""Converts a second timestamp and returns a datetime object of the current time in UTC"""
return _dt.datetime.strptime(
time,
"%Y-%m-%dT%H:%M:%S%z",
)

class PrecisionTimestamp(_enum.Enum):
"""
The status of a token.
"""

def is_precision_timestamp(time: str) -> bool:
NANOSECONDS = "NANOSECONDS"

MICROSECONDS = "MICROSECONDS"

SECONDS = "SECONDS"


def get_precision_timestamp(time: str) -> PrecisionTimestamp:
"""Return a bool which indicates if the timestamp is in nanoseconds"""
# Split the string into date-time and fraction of second
try:
_, s_fraction = time.split(".")
except ValueError:
return False # If there's no decimal, it's not a nanosecond timestamp.
return PrecisionTimestamp.SECONDS

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

# If the fraction is more than 6 digits long, it's a nanosecond timestamp
return len(s_fraction) > 6
if len(s_fraction) > 6:
return PrecisionTimestamp.NANOSECONDS
else:
return PrecisionTimestamp.MICROSECONDS


def timestamp_conversion(time: str) -> _dt.datetime:
"""Converts a timestamp and returns a datetime object of the current time in UTC"""
precision_timestamp = get_precision_timestamp(time)

if precision_timestamp == PrecisionTimestamp.NANOSECONDS:
return nanoseconds_timestamp_conversion(time)
elif precision_timestamp == PrecisionTimestamp.MICROSECONDS:
return microsecond_timestamp_conversion(time)
elif precision_timestamp == PrecisionTimestamp.SECONDS:
return second_timestamp_conversion(time)

def microsecond_timestamp_conversion(time: str) -> _dt.datetime:
"""Converts a microsecond timestamp and returns a datetime object of the current time in UTC"""
Expand Down
46 changes: 36 additions & 10 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
Internal utils tests.
"""
from os import environ, path
from firebase_functions.private.util import firebase_config, microsecond_timestamp_conversion, nanoseconds_timestamp_conversion, is_precision_timestamp, normalize_path, deep_merge
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
import datetime as _dt

test_bucket = "python-functions-testing.appspot.com"
Expand Down Expand Up @@ -80,6 +80,23 @@ def test_nanosecond_conversion():
assert nanoseconds_timestamp_conversion(
input_timestamp) == expected_datetime

def test_second_conversion():
"""
Testing seconds_timestamp_conversion works as intended
"""
timestamps = [
("2023-01-01T12:34:56Z", "2023-01-01T12:34:56Z"),
("2023-02-14T14:37:52Z", "2023-02-14T14:37:52Z"),
("2023-03-21T06:43:58Z", "2023-03-21T06:43:58Z"),
("2023-10-06T07:00:00Z", "2023-10-06T07:00:00Z"),
]

for input_timestamp, expected_output in timestamps:
expected_datetime = _dt.datetime.strptime(expected_output,
"%Y-%m-%dT%H:%M:%SZ")
expected_datetime = expected_datetime.replace(tzinfo=_dt.timezone.utc)
assert second_timestamp_conversion(
input_timestamp) == expected_datetime

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

assert is_precision_timestamp(microsecond_timestamp1) is False
assert is_precision_timestamp(microsecond_timestamp2) is False
assert is_precision_timestamp(microsecond_timestamp3) is False
assert is_precision_timestamp(microsecond_timestamp4) is False
assert is_precision_timestamp(nanosecond_timestamp1) is True
assert is_precision_timestamp(nanosecond_timestamp2) is True
assert is_precision_timestamp(nanosecond_timestamp3) is True
assert is_precision_timestamp(nanosecond_timestamp4) is True
second_timestamp1 = "2023-01-01T12:34:56Z"
second_timestamp2 = "2023-02-14T14:37:52Z"
second_timestamp3 = "2023-03-21T06:43:58Z"
second_timestamp4 = "2023-08-15T22:22:22Z"

assert get_precision_timestamp(microsecond_timestamp1) is PrecisionTimestamp.MICROSECONDS
assert get_precision_timestamp(microsecond_timestamp2) is PrecisionTimestamp.MICROSECONDS
assert get_precision_timestamp(microsecond_timestamp3) is PrecisionTimestamp.MICROSECONDS
assert get_precision_timestamp(microsecond_timestamp4) is PrecisionTimestamp.MICROSECONDS
assert get_precision_timestamp(nanosecond_timestamp1) is PrecisionTimestamp.NANOSECONDS
assert get_precision_timestamp(nanosecond_timestamp2) is PrecisionTimestamp.NANOSECONDS
assert get_precision_timestamp(nanosecond_timestamp3) is PrecisionTimestamp.NANOSECONDS
assert get_precision_timestamp(nanosecond_timestamp4) is PrecisionTimestamp.NANOSECONDS
assert get_precision_timestamp(second_timestamp1) is PrecisionTimestamp.SECONDS
assert get_precision_timestamp(second_timestamp2) is PrecisionTimestamp.SECONDS
assert get_precision_timestamp(second_timestamp3) is PrecisionTimestamp.SECONDS
assert get_precision_timestamp(second_timestamp4) is PrecisionTimestamp.SECONDS


def test_normalize_document_path():
"""
Testing "document" path passed to Firestore event listener
Testing "document" path passed to Firestore event listener
is normalized.
"""
test_path = "/test/document/"
Expand Down