Skip to content

fix: conversion to handle nanosecond precision timestamp #109

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 11 commits into from
Jun 28, 2023
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
37 changes: 37 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,42 @@ 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", "").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 more than 6 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