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
14 changes: 9 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,15 @@ 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_nanoseconds_timestamp(time)

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

if _DEFAULT_APP_NAME not in _apps:
initialize_app()
Expand Down
46 changes: 46 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 @@ -308,3 +309,48 @@ def firebase_config() -> None | FirebaseConfig:
f'FIREBASE_CONFIG JSON string "{json_str}" is not valid json. {err}'
) from err
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"""

# Split the string into date-time and nanoseconds
s_datetime, s_ns = time.split(".")

# Split the nanoseconds from the timezone specifier ('Z')
s_ns, _ = s_ns.split("Z")

# Only take the first 6 digits of the nanoseconds
s_ns = s_ns[:6]

# Put the string back together
s_processed = f"{s_datetime}.{s_ns}Z"

# Now parse the date-time string
event_time = _dt.datetime.strptime(s_processed, "%Y-%m-%dT%H:%M:%S.%fZ")

# strptime assumes local time, we know it's UTC and so:
event_time = event_time.replace(tzinfo=_dt.timezone.utc)

return event_time


def is_nanoseconds_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) == 9


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",
)
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
from firebase_functions.private.util import firebase_config, microsecond_timestamp_conversion, nanoseconds_timestamp_conversion, is_nanoseconds_timestamp
import datetime as _dt

test_bucket = "python-functions-testing.appspot.com"
test_config_file = path.join(path.dirname(path.realpath(__file__)),
Expand All @@ -40,3 +41,65 @@ def test_firebase_config_loads_from_env_file():
environ["FIREBASE_CONFIG"] = test_config_file
assert firebase_config().storage_bucket == test_bucket, (
"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_nanoseconds_timestamp(microsecond_timestamp1) is False
assert is_nanoseconds_timestamp(microsecond_timestamp2) is False
assert is_nanoseconds_timestamp(microsecond_timestamp3) is False
assert is_nanoseconds_timestamp(microsecond_timestamp4) is False
assert is_nanoseconds_timestamp(nanosecond_timestamp1) is True
assert is_nanoseconds_timestamp(nanosecond_timestamp2) is True
assert is_nanoseconds_timestamp(nanosecond_timestamp3) is True
assert is_nanoseconds_timestamp(nanosecond_timestamp4) is True