# # Utilities to handle timestamps / durations from/to integers and strings # # datetime.{datetime,timedelta} are powerful tools, but these objects are not # natively marshalled over xmlrpc # from types import StringTypes import time, calendar import datetime from PLC.Faults import * from PLC.Parameter import Parameter, Mixed # a dummy class mostly used as a namespace class Timestamp: debug=False # debug=True # this is how we expose times to SQL sql_format = "%Y-%m-%d %H:%M:%S" sql_format_utc = "%Y-%m-%d %H:%M:%S UTC" # this one (datetime.isoformat) would work too but that's less readable - we support this input though iso_format = "%Y-%m-%dT%H:%M:%S" # sometimes it's convenient to understand more formats input_formats = [ sql_format, sql_format_utc, iso_format, "%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M UTC", "%Y-%m-%d %H:%M:%S.%f" ] # for timestamps we usually accept either an int, or an ISO string, # the datetime.datetime stuff can in general be used locally, # but not sure it can be marshalled over xmlrpc though @staticmethod def Parameter (doc): return Mixed (Parameter (int, doc + " (unix timestamp)"), Parameter (str, doc + " (formatted as %s)"%Timestamp.sql_format), ) @staticmethod def sql_validate (input, timezone=False, check_future = False): """ Validates the specified GMT timestamp, returns a standardized string suitable for SQL input. Input may be a number (seconds since UNIX epoch back in 1970, or a string (in one of the supported input formats). If timezone is True, the resulting string contains timezone information, which is hard-wired as 'UTC' If check_future is True, raises an exception if timestamp is in the past. Returns a GMT timestamp string suitable to feed SQL. """ if not timezone: output_format = Timestamp.sql_format else: output_format = Timestamp.sql_format_utc if Timestamp.debug: print 'sql_validate, in:',input, if isinstance(input, StringTypes): sql='' # calendar.timegm() is the inverse of time.gmtime() for time_format in Timestamp.input_formats: try: timestamp = calendar.timegm(time.strptime(input, time_format)) sql = time.strftime(output_format, time.gmtime(timestamp)) break # wrong format: ignore except ValueError: pass # could not parse it if not sql: raise PLCInvalidArgument, "Cannot parse timestamp %r - not in any of %r formats"%(input,Timestamp.input_formats) elif isinstance (input,(int,long,float)): try: timestamp = long(input) sql = time.strftime(output_format, time.gmtime(timestamp)) except Exception,e: raise PLCInvalidArgument, "Timestamp %r not recognized -- %r"%(input,e) else: raise PLCInvalidArgument, "Timestamp %r - unsupported type %r"%(input,type(input)) if check_future and input < time.time(): raise PLCInvalidArgument, "'%s' not in the future" % sql if Timestamp.debug: print 'sql_validate, out:',sql return sql @staticmethod def sql_validate_utc (timestamp): "For convenience, return sql_validate(intput, timezone=True, check_future=False)" return Timestamp.sql_validate (timestamp, timezone=True, check_future=False) @staticmethod def cast_long (input): """ Translates input timestamp as a unix timestamp. Input may be a number (seconds since UNIX epoch, i.e., 1970-01-01 00:00:00 GMT), a string (in one of the supported input formats above). """ if Timestamp.debug: print 'cast_long, in:',input, if isinstance(input, StringTypes): timestamp=0 for time_format in Timestamp.input_formats: try: result=calendar.timegm(time.strptime(input, time_format)) if Timestamp.debug: print 'out:',result return result # wrong format: ignore except ValueError: pass raise PLCInvalidArgument, "Cannot parse timestamp %r - not in any of %r formats"%(input,Timestamp.input_formats) elif isinstance (input,(int,long,float)): result=long(input) if Timestamp.debug: print 'out:',result return result else: raise PLCInvalidArgument, "Timestamp %r - unsupported type %r"%(input,type(input)) def utcparse(input): """ Translate a string into a time using dateutil.parser.parse but make sure it's in UTC time and strip the timezone, so that it's compatible with normal datetime.datetime objects. For safety this can also handle inputs that are either timestamps, or datetimes """ # prepare the input for the checks below by # casting strings ('1327098335') to ints if isinstance(input, StringTypes): try: input = int(input) except ValueError: pass if isinstance (input, datetime.datetime): return input elif isinstance (input, StringTypes): t = dateutil.parser.parse(input) if t.utcoffset() is not None: t = t.utcoffset() + t.replace(tzinfo=None) return t elif isinstance (input, (int,float,long)): return datetime.datetime.fromtimestamp(input) else: raise def string(input): return datetime.datetime.strftime(Timestamp.utcparse(input), Timestamp.sql_format) # utility for displaying durations # be consistent in avoiding the datetime stuff class Duration: MINUTE = 60 HOUR = 3600 DAY = 3600*24 @staticmethod def to_string(duration): result=[] left=duration (days,left) = divmod(left,Duration.DAY) if days: result.append("%d d)"%td.days) (hours,left) = divmod (left,Duration.HOUR) if hours: result.append("%d h"%hours) (minutes, seconds) = divmod (left, Duration.MINUTE) if minutes: result.append("%d m"%minutes) if seconds: result.append("%d s"%seconds) if not result: result = ['void'] return "-".join(result) @staticmethod def validate (duration): # support seconds only for now, works for int/long/str try: return long (duration) except: raise PLCInvalidArgument, "Could not parse duration %r"%duration