From d110b84f2d3ee01f086027a6a481d3be96368b44 Mon Sep 17 00:00:00 2001 From: Daniel Cabello Date: Thu, 12 Oct 2023 17:18:03 +0000 Subject: [PATCH 1/2] Handle optional fractional seconds --- src/firebase_functions/firestore_fn.py | 7 +-- src/firebase_functions/private/_alerts_fn.py | 33 ++++---------- src/firebase_functions/private/util.py | 38 ++++++++++++++-- tests/test_util.py | 46 +++++++++++++++----- 4 files changed, 80 insertions(+), 44 deletions(-) diff --git a/src/firebase_functions/firestore_fn.py b/src/firebase_functions/firestore_fn.py index 935f27d..4b66f15 100644 --- a/src/firebase_functions/firestore_fn.py +++ b/src/firebase_functions/firestore_fn.py @@ -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() diff --git a/src/firebase_functions/private/_alerts_fn.py b/src/firebase_functions/private/_alerts_fn.py index 19a69bb..4d39bc2 100644 --- a/src/firebase_functions/private/_alerts_fn.py +++ b/src/firebase_functions/private/_alerts_fn.py @@ -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 @@ -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"]) ) @@ -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"] @@ -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"], @@ -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, ) @@ -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"], } diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index 80d0ce4..9e5308f 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -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""" diff --git a/tests/test_util.py b/tests/test_util.py index e524033..504451a 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -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" @@ -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(): """ @@ -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/" From cd65454cfc216a9daeb25707758030db274ce967 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 16 Oct 2023 15:14:50 -0700 Subject: [PATCH 2/2] Run formatter. --- src/firebase_functions/private/_alerts_fn.py | 35 +++++++---------- src/firebase_functions/private/util.py | 3 ++ tests/test_util.py | 41 +++++++++++++------- 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/src/firebase_functions/private/_alerts_fn.py b/src/firebase_functions/private/_alerts_fn.py index 4d39bc2..4a0c5dc 100644 --- a/src/firebase_functions/private/_alerts_fn.py +++ b/src/firebase_functions/private/_alerts_fn.py @@ -102,11 +102,10 @@ def new_nonfatal_issue_payload_from_ce_payload(payload: dict): def regression_alert_payload_from_ce_payload(payload: dict): from firebase_functions.alerts.crashlytics_fn import RegressionAlertPayload - return RegressionAlertPayload( - type=payload["type"], - issue=issue_from_ce_payload(payload["issue"]), - resolve_time= _util.timestamp_conversion(payload["resolveTime"]) - ) + return RegressionAlertPayload(type=payload["type"], + issue=issue_from_ce_payload(payload["issue"]), + resolve_time=_util.timestamp_conversion( + payload["resolveTime"])) def trending_issue_details_from_ce_payload(payload: dict): @@ -179,7 +178,7 @@ def firebase_alert_data_from_ce(event_dict: dict,) -> FirebaseAlertData: return FirebaseAlertData( create_time=_util.timestamp_conversion(event_dict["createTime"]), end_time=_util.timestamp_conversion(event_dict["endTime"]) - if "endTime" in event_dict else None, + if "endTime" in event_dict else None, payload=alert_payload, ) @@ -190,22 +189,14 @@ def event_from_ce_helper(raw: _ce.CloudEvent, cls, app_id=True): event_dict = {**event_data, **event_attributes} alert_type: str = event_dict["alerttype"] event_kwargs = { - "alert_type": - alert_type, - "data": - firebase_alert_data_from_ce(event_dict), - "id": - event_dict["id"], - "source": - event_dict["source"], - "specversion": - event_dict["specversion"], - "subject": - event_dict["subject"] if "subject" in event_dict else None, - "time": - _util.timestamp_conversion(event_dict["time"]), - "type": - event_dict["type"], + "alert_type": alert_type, + "data": firebase_alert_data_from_ce(event_dict), + "id": event_dict["id"], + "source": event_dict["source"], + "specversion": event_dict["specversion"], + "subject": event_dict["subject"] if "subject" in event_dict else None, + "time": _util.timestamp_conversion(event_dict["time"]), + "type": event_dict["type"], } if app_id: event_kwargs["app_id"] = event_dict.get("appid") diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index 9e5308f..2533b1b 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -336,6 +336,7 @@ 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( @@ -343,6 +344,7 @@ def second_timestamp_conversion(time: str) -> _dt.datetime: "%Y-%m-%dT%H:%M:%S%z", ) + class PrecisionTimestamp(_enum.Enum): """ The status of a token. @@ -385,6 +387,7 @@ def timestamp_conversion(time: str) -> _dt.datetime: 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""" return _dt.datetime.strptime( diff --git a/tests/test_util.py b/tests/test_util.py index 504451a..0a8dcbb 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -80,6 +80,7 @@ def test_nanosecond_conversion(): assert nanoseconds_timestamp_conversion( input_timestamp) == expected_datetime + def test_second_conversion(): """ Testing seconds_timestamp_conversion works as intended @@ -95,8 +96,8 @@ def test_second_conversion(): 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 + assert second_timestamp_conversion(input_timestamp) == expected_datetime + def test_is_nanoseconds_timestamp(): """ @@ -117,18 +118,30 @@ def test_is_nanoseconds_timestamp(): 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 + 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():