Solved

Reading FME datetime attributes as a Python datetimes

  • 21 June 2023
  • 5 replies
  • 83 views

Userlevel 1
Badge +9

One of the main pain points I've been having so far with using Python with FME is getting datetime attributes from features as datetimes in Python. FME datetimes normally have a sub-second precision of 7 digits, up to 9but Python stops parsing after 6.

 

"When used with the strptime() method, the %f directive accepts from one to six digits and zero pads on the right. %f is an extension to the set of format characters in the C standard (but implemented separately in datetime objects, and therefore always available)."

        #
        # Unzoned
        #
        
        a_timestamp = feature.getAttribute('a_timestamp')
        print(repr(a_timestamp)) # '20230620161403.0020449'
        
        # ValueError: unconverted data remains: 9
        # (Python's strptime stops at 6 decimals)
        # (This will randomly work once out of 10 because trailing zeroes are trimmed)
        # datetime.strptime(a_timestamp, '%Y%m%d%H%M%S.%f')
        
        # This shouldn't be that complicated!
        # Also, the dot in the format string means this will break
        # if no seconds decimals are present
        datetime.strptime(a_timestamp[0:21], '%Y%m%d%H%M%S.%f')
        #
        # Zoned
        #
        
        a_zoned_timestamp = feature.getAttribute('a_zoned_timestamp')
        print(repr(a_zoned_timestamp)) # '20230620204757.833922+00:00'
        
        # ValueError: time data '20230620205748.6822671+00:00' does not match format '%Y%m%d%H%M%S.%f%z'
        #datetime.strptime(a_zoned_timestamp, '%Y%m%d%H%M%S.%f%z')
        
        # It works... *but at what cost?*
        # That also has 1/100 chance to break if the timestamp has 2 trailing zeroes
        # and is trimmed to 5 fractionnal digits
        datetime.strptime(a_zoned_timestamp[0:21] + a_zoned_timestamp[-6:], '%Y%m%d%H%M%S.%f%z')

Even if that wasn't an issue, having to constantly go back and forth between parsing and formatting datetimes makes manipulating these needlessly complicated. I'm surprised feature.getAttribute() can't handle auto-converting to/from Python's standard datetime class.

 

from datetime import datetime        
#
# Wishful thinking :(
#
 
# TypeError: desired type must be bool, int, float, str, bytes, bytearray
a_timestamp = feature.getAttribute('a_timestamp', datetime)

Am I missing something regarding the "obvious" way of dealing with this? Even discounting the length issues, the string parsing/formatting dance gets old pretty quickly. Even just some fmeobjects.STRPTIME_DATETIMETZ_FORMAT constant would be a huge time saver, here.

icon

Best answer by vlroyrenn 24 July 2023, 17:34

View original

5 replies

Userlevel 5
Badge +28

hehe, that's no fun at all. Although, let's be clear this is quite clearly a python issue not FME. Python should be able to handle this.

 

But I have actually be after a reliable way to reduce the precision of the seconds for other reasons. I've actually defaulted to converting everything to %s in FME. (seconds from epoc in UTC (or %Es if unzoned)).

 

In Python you can also just add the number of seconds to the origin time and get what you want. There isn't the issue with precision.

import fme
import fmeobjects
import datetime
from datetime import datetime, timedelta, timezone
 
 
    def input(self, feature):
        timestamp=feature.getAttribute('_timestamp')
        print(timestamp) #1687356496.1503231
        
        pytime=datetime(1970,1,1,tzinfo=timezone.utc) + timedelta(seconds=float(timestamp))
        print(pytime) #2023-06-21 14:08:16.150323+00:00
        self.pyoutput(feature)

There is probably still a better way but this is at least workable. Unfortunately this means you have to either keep track if the data is zoned or unzoned as it's not kept in the attribute (but you don't need to know the specific timezone). 

Userlevel 1
Badge +9

hehe, that's no fun at all. Although, let's be clear this is quite clearly a python issue not FME. Python should be able to handle this.

 

But I have actually be after a reliable way to reduce the precision of the seconds for other reasons. I've actually defaulted to converting everything to %s in FME. (seconds from epoc in UTC (or %Es if unzoned)).

 

In Python you can also just add the number of seconds to the origin time and get what you want. There isn't the issue with precision.

import fme
import fmeobjects
import datetime
from datetime import datetime, timedelta, timezone
 
 
    def input(self, feature):
        timestamp=feature.getAttribute('_timestamp')
        print(timestamp) #1687356496.1503231
        
        pytime=datetime(1970,1,1,tzinfo=timezone.utc) + timedelta(seconds=float(timestamp))
        print(pytime) #2023-06-21 14:08:16.150323+00:00
        self.pyoutput(feature)

There is probably still a better way but this is at least workable. Unfortunately this means you have to either keep track if the data is zoned or unzoned as it's not kept in the attribute (but you don't need to know the specific timezone). 

> Although, let's be clear this is quite clearly a python issue not FME. Python should be able to handle this.

 

The precision issue, sure, but I'd still say fmeobjects should be able to handle datetime marshalling automatically.

 

> But I have actually be after a reliable way to reduce the precision of the seconds for other reasons. I've actually defaulted to converting everything to %s in FME. (seconds from epoc in UTC (or %Es if unzoned)).

 

That's actually a pretty good idea, though with the caveats that you're raising regarding loss of timezone information, plus how dates become unreadable in the feature inspector when you do that, plus how you would still need to convert them back to dates before writing them in a database or something.

 

It's an interesting workaround, though, and I'll be sure to keep it in mind.

 

> pytime=datetime(1970,1,1,tzinfo=timezone.utc) + timedelta(seconds=float(timestamp))

 

FYI, you can use datetime.utcfromtimestamp(timestamp).

Userlevel 1
Badge +9

Ok, so, as I found out going through the SDK doc and letting Pycharm generate .pyc function list stubs for me, there is a correct way to do this.

 

import fmegeneral.fmeutil as fmeutil
 
py_datetime = fmeutil.fmeDateToPython(fme_datetime)
 
# fme_datetime: "20230724152232.3451145+00:00"
# py_datetime: (datetime.datetime(2023, 7, 24, 15, 22, 32, 345114, tzinfo=<fmegeneral.fmeutil.FMETZInfo object at 0x000000000EB8E010>), True, True)

That's it. I've not seen it documented anywhere, I've not seen it mentionned in these forums even once. It handles decimal rounding, timezone offsets (using its own FMETZInfo offset object), detecting whether you're parsing days or naive datetimes or timestamps, everything! It's right there and it does everything as one would expect it to, so why is it that nobody seems to know about this?

 

Issue solved. Hopefully that will give some visibility to this fmeDateToPython function.

 

EDIT: Other useful functions in there include:

  • isoTimestampToFMEFormat(isoTimestamp)
  • pythonDateTimeToFMEFormat(theDateTime)
Userlevel 5
Badge +28

Ok, so, as I found out going through the SDK doc and letting Pycharm generate .pyc function list stubs for me, there is a correct way to do this.

 

import fmegeneral.fmeutil as fmeutil
 
py_datetime = fmeutil.fmeDateToPython(fme_datetime)
 
# fme_datetime: "20230724152232.3451145+00:00"
# py_datetime: (datetime.datetime(2023, 7, 24, 15, 22, 32, 345114, tzinfo=<fmegeneral.fmeutil.FMETZInfo object at 0x000000000EB8E010>), True, True)

That's it. I've not seen it documented anywhere, I've not seen it mentionned in these forums even once. It handles decimal rounding, timezone offsets (using its own FMETZInfo offset object), detecting whether you're parsing days or naive datetimes or timestamps, everything! It's right there and it does everything as one would expect it to, so why is it that nobody seems to know about this?

 

Issue solved. Hopefully that will give some visibility to this fmeDateToPython function.

 

EDIT: Other useful functions in there include:

  • isoTimestampToFMEFormat(isoTimestamp)
  • pythonDateTimeToFMEFormat(theDateTime)

Thanks for sharing this. I'll have to see what other stuff is in fmegeneral. Interesting, I've personally not looked into the SDK but these kinds of functions look pretty helpful. 

Userlevel 1
Badge +9

Thanks for sharing this. I'll have to see what other stuff is in fmegeneral. Interesting, I've personally not looked into the SDK but these kinds of functions look pretty helpful. 

As it turns out, the source for fmegeneral used to be in the repository for fmetools, so you can seill see what the actual, commented and documented code looks like and how each function works or not.

 

However, most of these are somewhat flawed. PythonDatetimeToFmeFormat can only encode datetimes (not date or time objects) and fails when tzinfo is not an instance of FMETZInfo. fmeDateToPython is more reliable, but the RegEx they use doesn't match their own datetime format specification ("20230303.123456" is valid but "20230303101010-0430" fails to capture the offset properly and the offset is registered as "-04:00").

 

Still, it's definitely better and generally more reliable than anything I would have written with strptime.

 

Their regex should probably read more like this, though:

^(?P<date>\d{8})?(?:(?P<time>\d{6})(?:\.(?P<us>\d+))?(?:(?P<tzs>[\-+])(?P<tzh>[01][0-9])(?::?(?P<tzm>[0-5][0-9]))?)?)?$

 

Reply