Skip to content
13 changes: 8 additions & 5 deletions src/firebase_functions/firestore_fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import dataclasses as _dataclass
import functools as _functools
import typing as _typing
import datetime as _dt
import google.events.cloud.firestore as _firestore
import google.cloud.firestore_v1 as _firestore_v1
import firebase_functions.private.util as _util
Expand Down Expand Up @@ -111,10 +110,14 @@ def _firestore_endpoint_handler(
event_namespace = event_attributes["namespace"]
event_document = event_attributes["document"]
event_database = event_attributes["database"]
event_time = _dt.datetime.strptime(
event_attributes["time"],
"%Y-%m-%dT%H:%M:%S.%f%z",
)

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)

if _DEFAULT_APP_NAME not in _apps:
initialize_app()
Expand Down
36 changes: 36 additions & 0 deletions src/firebase_functions/private/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import json as _json
import typing as _typing
import dataclasses as _dataclasses
import datetime as _dt
import enum as _enum
from flask import Request as _Request
from functions_framework import logging as _logging
Expand Down Expand Up @@ -310,6 +311,41 @@ def firebase_config() -> None | FirebaseConfig:
return FirebaseConfig(storage_bucket=json_data.get("storageBucket"))


def nanoseconds_timestamp_conversion(time: str) -> _dt.datetime:
"""Converts a nanosecond timestamp and returns a datetime object of the current time in UTC"""

# Separate the date and time part from the nanoseconds.
datetime_str, nanosecond_str = time.replace("Z", "").split(".")
# Parse the date and time part of the string.
event_time = _dt.datetime.strptime(datetime_str, "%Y-%m-%dT%H:%M:%S")
# Add the microseconds and timezone.
event_time = event_time.replace(microsecond=int(nanosecond_str[:6]),
tzinfo=_dt.timezone.utc)

return event_time


def is_precision_timestamp(time: str) -> bool:
"""Return a bool which indicates if the timestamp is in nanoseconds"""
# Split the string into date-time and fraction of second
_, s_fraction = time.split(".")

# 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 9 digits long, it's a nanosecond timestamp
return len(s_fraction) > 6


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(
time,
"%Y-%m-%dT%H:%M:%S.%f%z",
)


def normalize_path(path: str) -> str:
"""
Normalize a path string to a consistent format.
Expand Down
65 changes: 64 additions & 1 deletion tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
Internal utils tests.
"""
from os import environ, path
from firebase_functions.private.util import firebase_config, normalize_path
from firebase_functions.private.util import firebase_config, microsecond_timestamp_conversion, nanoseconds_timestamp_conversion, is_precision_timestamp, normalize_path
import datetime as _dt

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


def test_microsecond_conversion():
"""
Testing microsecond_timestamp_conversion works as intended
"""
timestamps = [
("2023-06-20T10:15:22.396358Z", "2023-06-20T10:15:22.396358Z"),
("2021-02-20T11:23:45.987123Z", "2021-02-20T11:23:45.987123Z"),
("2022-09-18T09:15:38.246824Z", "2022-09-18T09:15:38.246824Z"),
("2010-09-18T09:15:38.246824Z", "2010-09-18T09:15:38.246824Z"),
]

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


def test_nanosecond_conversion():
"""
Testing nanoseconds_timestamp_conversion works as intended
"""
timestamps = [
("2023-01-01T12:34:56.123456789Z", "2023-01-01T12:34:56.123456Z"),
("2023-02-14T14:37:52.987654321Z", "2023-02-14T14:37:52.987654Z"),
("2023-03-21T06:43:58.564738291Z", "2023-03-21T06:43:58.564738Z"),
("2023-08-15T22:22:22.222222222Z", "2023-08-15T22:22:22.222222Z"),
]

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


def test_is_nanoseconds_timestamp():
"""
Testing is_nanoseconds_timestamp works as intended
"""
microsecond_timestamp1 = "2023-06-20T10:15:22.396358Z"
microsecond_timestamp2 = "2021-02-20T11:23:45.987123Z"
microsecond_timestamp3 = "2022-09-18T09:15:38.246824Z"
microsecond_timestamp4 = "2010-09-18T09:15:38.246824Z"

nanosecond_timestamp1 = "2023-01-01T12:34:56.123456789Z"
nanosecond_timestamp2 = "2023-02-14T14:37:52.987654321Z"
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


def test_normalize_document_path():
"""
Testing "document" path passed to Firestore event listener
Expand Down