Added datetime format
[plcapi.git] / PLC / Timestamp.py
1 #
2 # Utilities to handle timestamps / durations from/to integers and strings
3 #
4 # datetime.{datetime,timedelta} are powerful tools, but these objects are not
5 # natively marshalled over xmlrpc
6 #
7
8 from types import StringTypes
9 import time, calendar
10 import datetime
11
12 from PLC.Faults import *
13 from PLC.Parameter import Parameter, Mixed
14
15 # a dummy class mostly used as a namespace
16 class Timestamp:
17
18     debug=False
19 #    debug=True
20
21     # this is how we expose times to SQL
22     sql_format = "%Y-%m-%d %H:%M:%S"
23     sql_format_utc = "%Y-%m-%d %H:%M:%S UTC"
24     
25     # this one (datetime.isoformat) would work too but that's less readable - we support this input though
26     iso_format = "%Y-%m-%dT%H:%M:%S"
27     # sometimes it's convenient to understand more formats
28     input_formats = [ sql_format,
29                       sql_format_utc,
30                       iso_format,
31                       "%Y-%m-%d %H:%M",
32                       "%Y-%m-%d %H:%M UTC",
33                       "%Y-%m-%d %H:%M:%S.%f"
34                       ]
35
36     # for timestamps we usually accept either an int, or an ISO string,
37     # the datetime.datetime stuff can in general be used locally,
38     # but not sure it can be marshalled over xmlrpc though
39
40     @staticmethod
41     def Parameter (doc):
42         return Mixed (Parameter (int, doc + " (unix timestamp)"),
43                       Parameter (str, doc + " (formatted as %s)"%Timestamp.sql_format),
44                       )
45
46     @staticmethod
47     def sql_validate (input, timezone=False, check_future = False):
48         """
49         Validates the specified GMT timestamp, returns a
50         standardized string suitable for SQL input.
51
52         Input may be a number (seconds since UNIX epoch back in 1970,
53         or a string (in one of the supported input formats).
54
55         If timezone is True, the resulting string contains
56         timezone information, which is hard-wired as 'UTC'
57
58         If check_future is True, raises an exception if timestamp is in
59         the past.
60
61         Returns a GMT timestamp string suitable to feed SQL.
62         """
63
64         if not timezone: output_format = Timestamp.sql_format
65         else:            output_format = Timestamp.sql_format_utc
66
67         if Timestamp.debug: print 'sql_validate, in:',input,
68         if isinstance(input, StringTypes):
69             sql=''
70             # calendar.timegm() is the inverse of time.gmtime()
71             for time_format in Timestamp.input_formats:
72                 try:
73                     timestamp = calendar.timegm(time.strptime(input, time_format))
74                     sql = time.strftime(output_format, time.gmtime(timestamp))
75                     break
76                 # wrong format: ignore
77                 except ValueError: pass
78             # could not parse it
79             if not sql:
80                 raise PLCInvalidArgument, "Cannot parse timestamp %r - not in any of %r formats"%(input,Timestamp.input_formats)
81         elif isinstance (input,(int,long,float)):
82             try:
83                 timestamp = long(input)
84                 sql = time.strftime(output_format, time.gmtime(timestamp))
85             except Exception,e:
86                 raise PLCInvalidArgument, "Timestamp %r not recognized -- %r"%(input,e)
87         else:
88             raise PLCInvalidArgument, "Timestamp %r - unsupported type %r"%(input,type(input))
89
90         if check_future and input < time.time():
91             raise PLCInvalidArgument, "'%s' not in the future" % sql
92
93         if Timestamp.debug: print 'sql_validate, out:',sql
94         return sql
95
96     @staticmethod
97     def sql_validate_utc (timestamp):
98         "For convenience, return sql_validate(intput, timezone=True, check_future=False)"
99         return Timestamp.sql_validate (timestamp, timezone=True, check_future=False)
100
101
102     @staticmethod
103     def cast_long (input):
104         """
105         Translates input timestamp as a unix timestamp.
106
107         Input may be a number (seconds since UNIX epoch, i.e., 1970-01-01
108         00:00:00 GMT), a string (in one of the supported input formats above).
109
110         """
111         if Timestamp.debug: print 'cast_long, in:',input,
112         if isinstance(input, StringTypes):
113             timestamp=0
114             for time_format in Timestamp.input_formats:
115                 try:
116                     result=calendar.timegm(time.strptime(input, time_format))
117                     if Timestamp.debug: print 'out:',result
118                     return result
119                 # wrong format: ignore
120                 except ValueError: pass
121             raise PLCInvalidArgument, "Cannot parse timestamp %r - not in any of %r formats"%(input,Timestamp.input_formats)
122         elif isinstance (input,(int,long,float)):
123             result=long(input)
124             if Timestamp.debug: print 'out:',result
125             return result
126         else:
127             raise PLCInvalidArgument, "Timestamp %r - unsupported type %r"%(input,type(input))
128         
129     def utcparse(input):
130         """ Translate a string into a time using dateutil.parser.parse but make 
131             sure it's in UTC time and strip the timezone, so that it's compatible 
132             with normal datetime.datetime objects.
133
134             For safety this can also handle inputs that are either timestamps, or 
135             datetimes
136         """
137         # prepare the input for the checks below by
138         # casting strings ('1327098335') to ints
139         if isinstance(input, StringTypes):
140             try:
141                 input = int(input)
142             except ValueError:
143                 pass
144
145         if isinstance (input, datetime.datetime):
146             return input
147         elif isinstance (input, StringTypes):
148             t = dateutil.parser.parse(input)
149             if t.utcoffset() is not None:
150                 t = t.utcoffset() + t.replace(tzinfo=None)
151             return t
152         elif isinstance (input, (int,float,long)):
153             return datetime.datetime.fromtimestamp(input)
154         else:
155             raise
156
157     def string(input):
158         return datetime.datetime.strftime(Timestamp.utcparse(input), Timestamp.sql_format)
159
160 # utility for displaying durations
161 # be consistent in avoiding the datetime stuff
162 class Duration:
163
164     MINUTE = 60
165     HOUR = 3600
166     DAY = 3600*24
167
168     @staticmethod
169     def to_string(duration):
170         result=[]
171         left=duration
172         (days,left) = divmod(left,Duration.DAY)
173         if days:    result.append("%d d)"%td.days)
174         (hours,left) = divmod (left,Duration.HOUR)
175         if hours:   result.append("%d h"%hours)
176         (minutes, seconds) = divmod (left, Duration.MINUTE)
177         if minutes: result.append("%d m"%minutes)
178         if seconds: result.append("%d s"%seconds)
179         if not result: result = ['void']
180         return "-".join(result)
181
182     @staticmethod
183     def validate (duration):
184         # support seconds only for now, works for int/long/str
185         try:
186             return long (duration)
187         except:
188             raise PLCInvalidArgument, "Could not parse duration %r"%duration