pythonfirebasetransactionsgoogle-cloud-firestorefirebase-admin

AttributeError: _nanosecond when updating a datetime in transaction


So I am trying to update a datetime field in cloud firestore via cloud function as follows:

transaction.update(doc_ref, {'dateTimeField1': dateTimeValue})

Google sends datetime objects as string in event param of the cloud function in the format %Y-%m-%dT%H:%M:%SZ or %Y-%m-%dT%H:%M:%S.%fZ.
For Example: 2019-01-25T15:25:03.881Z

I am converting it to datetime object as follows:

try:
    datetime_obj = datetime.datetime.strptime(datetime_obj, '%Y-%m-%dT%H:%M:%S.%fZ')
except:
    datetime_obj = datetime.datetime.strptime(datetime_obj, '%Y-%m-%dT%H:%M:%SZ')
datetime_obj = datetime_obj.replace(tzinfo=timezone('UTC'))

But when I try to perform the operation I am greeted with the following error: AttributeError: _nanosecond

Traceback:
File "/env/local/lib/python3.7/site-packages/google/cloud/firestore_v1beta1/batch.py", line 112, in update reference._document_path, field_updates, option File "/env/local/lib/python3.7/site-packages/google/cloud/firestore_v1beta1/_helpers.py", line 822, in pbs_for_update update_pb = extractor.get_update_pb(document_path) File "/env/local/lib/python3.7/site-packages/google/cloud/firestore_v1beta1/_helpers.py", line 459, in get_update_pb name=document_path, fields=encode_dict(self.set_fields) File "/env/local/lib/python3.7/site-packages/google/cloud/firestore_v1beta1/_helpers.py", line 215, in encode_dict return {key: encode_value(value) for key, value in six.iteritems(values_dict)} File "/env/local/lib/python3.7/site-packages/google/cloud/firestore_v1beta1/_helpers.py", line 215, in <dictcomp> return {key: encode_value(value) for key, value in six.iteritems(values_dict)} File "/env/local/lib/python3.7/site-packages/google/cloud/firestore_v1beta1/_helpers.py", line 169, in encode_value return document_pb2.Value(timestamp_value=value.timestamp_pb()) File "/env/local/lib/python3.7/site-packages/google/api_core/datetime_helpers.py", line 278, in timestamp_pb nanos = self._nanosecond or self.microsecond * 1000 AttributeError: _nanosecond

Are datetimes allowed to be set via transactions or I am missing something here?

EDIT:
code snippet:

@firestore.transactional
def update_datetime_field(transaction, doc_ref, datetime_value):
    try:
        datetime_obj = datetime.datetime.strptime(datetime_value, '%Y-%m-%dT%H:%M:%S.%fZ')
    except:
        datetime_obj = datetime.datetime.strptime(datetime_value, '%Y-%m-%dT%H:%M:%SZ')
    datetime_obj = datetime_obj.replace(tzinfo=timezone('UTC'))
    # Example of datetime_obj -> datetime.datetime(2019, 1, 25, 15, 25, 3, 881000, tzinfo=<UTC>)
    transaction.update(doc_ref, {'datetimeField1': datetime_obj})
    return True

More info:

  1. The above code is triggered when a document is updated at say collection1/document1/collection2/document2
  2. datetime object is python's datetime from the standard libary
  3. I am trying to convert the date to UTC by changing the timezone using pytz

EDIT 2:

A better complete picture:

from firebase_admin import credentials, firestore

# initialize firebase admin sdk
creds = credentials.ApplicationDefault()
firebase_admin.initialize_app(creds,{'projectId': 'myProjectId'})


@firestore.transactional
def update_datetime_field(transaction, doc_ref, datetime_value):
    try:
        datetime_obj = datetime.datetime.strptime(datetime_value, '%Y-%m-%dT%H:%M:%S.%fZ')
    except:
        datetime_obj = datetime.datetime.strptime(datetime_value, '%Y-%m-%dT%H:%M:%SZ')
    datetime_obj = datetime_obj.replace(tzinfo=timezone('UTC'))
    # Example of datetime_obj -> datetime.datetime(2019, 1, 25, 15, 25, 3, 881000, tzinfo=<UTC>)
    transaction.update(doc_ref, {'datetimeField1': datetime_obj})
    return True

def update_datetime_in_transaction(event, context):
    datetime_value = event['value']['fields']['datetimeField1']['timestampValue']
    # this looks something like 2019-01-25T15:25:03.881Z

    # prepare document reference to document
    doc_ref = prepare_doc_ref(event, context)

    # update_datetime_field
    client = firestore.client()
    transaction = client.transaction()
    update_datetime_field(transaction, doc_ref, datetime_value)

    return True

EDIT 3:

Screenshot of event param: enter image description here

Screenshot of console:
enter image description here


Solution

  • So firestore python sdk expects the _nanosecond attribute which is not available in the python standard library's datetime at the moment (will be added in future. more details here)

    So after examining their codebase, I found a class called DatetimeWithNanoseconds which adds the nanoseconds support to traditional datetime object.

    The code for the class (datetime_helpers.py file in google/api_core) is as follows (some parts deleted intentionally for brevity):

    class DatetimeWithNanoseconds(datetime.datetime):
    """Track nanosecond in addition to normal datetime attrs.
    
    Nanosecond can be passed only as a keyword argument.
    """
    __slots__ = ('_nanosecond',)
    
    @classmethod
    def from_rfc3339(cls, stamp):
        with_nanos = _RFC3339_NANOS.match(stamp)
        if with_nanos is None:
            raise ValueError(
                'Timestamp: {}, does not match pattern: {}'.format(
                    stamp, _RFC3339_NANOS.pattern))
        bare = datetime.datetime.strptime(
            with_nanos.group('no_fraction'), _RFC3339_NO_FRACTION)
        fraction = with_nanos.group('nanos')
        if fraction is None:
            nanos = 0
        else:
            scale = 9 - len(fraction)
            nanos = int(fraction) * (10 ** scale)
        return cls(bare.year, bare.month, bare.day,
                   bare.hour, bare.minute, bare.second,
                   nanosecond=nanos, tzinfo=pytz.UTC)
    

    So now, I can use this class instead of datetime.datetime to parse the datetime sent as string in the event param of the cloud functions using the DatetimeWithNanoseconds.from_rfc3339(timestamp) method.

    Example:

    from google.api_core.datetime_helpers import DatetimeWithNanoseconds
    
    d1 = DatetimeWithNanoseconds.from_rfc3339('2019-01-25T15:25:03.881Z')
    print(d1)
    # DatetimeWithNanoseconds(2019, 1, 25, 15, 25, 3, 881000, tzinfo=<UTC>)
    

    The class also has rfc3339() method to give you the string representation.

    Example:

    d1.rfc3339()
    # 2019-01-25T15:25:03.881Z
    

    Alternative solution:

    You can use pandas.Timestamp() instead of DatetimeWithNanoseconds.from_rfc3339() too.

    Example:

    import pandas as pd
    
    d1 = pd.Timestamp('2019-01-25T15:25:03.881Z')
    print(d1)
    # Timestamp('2019-01-25 15:25:03.881000+0000', tz='UTC')
    

    I recommend using DatetimeWithNanoseconds as it comes along with the sdk and you needn't add an extra dependency of pandas in the requirements.txt which can increase the invocation latency during the cold start. More details here.

    Hopefully this helps.