First draft for leases
[plcapi.git] / PLC / Timestamp.py
diff --git a/PLC/Timestamp.py b/PLC/Timestamp.py
new file mode 100644 (file)
index 0000000..1b1f9ad
--- /dev/null
@@ -0,0 +1,161 @@
+#
+# Utilities to handle timestamps / durations from/to integers and strings
+#
+# $Id$
+# $URL$
+#
+
+#
+# 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",
+                      ]
+
+    # 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))
+
+
+# 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