Setting tag plcapi-7.0-0
[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 import time, calendar
9 import datetime
10
11 from PLC.Faults import *
12 from PLC.Parameter import Parameter, Mixed
13
14 # a dummy class mostly used as a namespace
15 class Timestamp:
16
17     debug = False
18 #    debug=True
19
20     # this is how we expose times to SQL
21     sql_format = "%Y-%m-%d %H:%M:%S"
22     sql_format_utc = "%Y-%m-%d %H:%M:%S UTC"
23     # this one (datetime.isoformat) would work too but that's less readable - we support this input though
24     iso_format = "%Y-%m-%dT%H:%M:%S"
25     # sometimes it's convenient to understand more formats
26     input_formats = [ sql_format,
27                       sql_format_utc,
28                       iso_format,
29                       "%Y-%m-%d %H:%M",
30                       "%Y-%m-%d %H:%M UTC",
31                       ]
32
33     # for timestamps we usually accept either an int, or an ISO string,
34     # the datetime.datetime stuff can in general be used locally,
35     # but not sure it can be marshalled over xmlrpc though
36
37     @staticmethod
38     def Parameter (doc):
39         return Mixed (Parameter (int, doc + " (unix timestamp)"),
40                       Parameter (str, doc + " (formatted as %s)"%Timestamp.sql_format),
41                       )
42
43     @staticmethod
44     def sql_validate (input, timezone=False, check_future = False):
45         """
46         Validates the specified GMT timestamp, returns a
47         standardized string suitable for SQL input.
48
49         Input may be a number (seconds since UNIX epoch back in 1970,
50         or a string (in one of the supported input formats).
51
52         If timezone is True, the resulting string contains
53         timezone information, which is hard-wired as 'UTC'
54
55         If check_future is True, raises an exception if timestamp is in
56         the past.
57
58         Returns a GMT timestamp string suitable to feed SQL.
59         """
60
61         output_format = (Timestamp.sql_format_utc if timezone
62                          else Timestamp.sql_format)
63
64         if Timestamp.debug:
65             print('sql_validate, in:', input, end=' ')
66         if isinstance(input, str):
67             sql = ''
68             # calendar.timegm() is the inverse of time.gmtime()
69             for time_format in Timestamp.input_formats:
70                 try:
71                     timestamp = calendar.timegm(time.strptime(input, time_format))
72                     sql = time.strftime(output_format, time.gmtime(timestamp))
73                     break
74                 # wrong format: ignore
75                 except ValueError:
76                     pass
77             # could not parse it
78             if not sql:
79                 raise PLCInvalidArgument("Cannot parse timestamp %r - not in any of %r formats"%(input,Timestamp.input_formats))
80         elif isinstance(input, (int, float)):
81             try:
82                 timestamp = int(input)
83                 sql = time.strftime(output_format, time.gmtime(timestamp))
84             except Exception as e:
85                 raise PLCInvalidArgument("Timestamp %r not recognized -- %r"%(input,e))
86         else:
87             raise PLCInvalidArgument("Timestamp %r - unsupported type %r"%(input,type(input)))
88
89         if check_future and input < time.time():
90             raise PLCInvalidArgument("'%s' not in the future" % sql)
91
92         if Timestamp.debug: print('sql_validate, out:',sql)
93         return sql
94
95     @staticmethod
96     def sql_validate_utc (timestamp):
97         "For convenience, return sql_validate(intput, timezone=True, check_future=False)"
98         return Timestamp.sql_validate (timestamp, timezone=True, check_future=False)
99
100
101     @staticmethod
102     def cast_long(input):
103         """
104         Translates input timestamp as a unix timestamp.
105
106         Input may be a number (seconds since UNIX epoch, i.e., 1970-01-01
107         00:00:00 GMT), a string (in one of the supported input formats above).
108
109         """
110         if Timestamp.debug:
111             print('cast_long, in:', input, end=' ')
112         if isinstance(input, str):
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:
118                         print('out:', result)
119                     return result
120                 # wrong format: ignore
121                 except ValueError:
122                     pass
123             raise PLCInvalidArgument("Cannot parse timestamp %r - not in any of %r formats"%(input, Timestamp.input_formats))
124         elif isinstance(input, (int, float)):
125             result = int(input)
126             if Timestamp.debug:
127                 print('out:',result)
128             return result
129         else:
130             raise PLCInvalidArgument("Timestamp %r - unsupported type %r"%(input,type(input)))
131
132
133 # utility for displaying durations
134 # be consistent in avoiding the datetime stuff
135 class Duration:
136
137     MINUTE = 60
138     HOUR = 3600
139     DAY = 3600*24
140
141     @staticmethod
142     def to_string(duration):
143         result=[]
144         left = duration
145         (days, left) = divmod(left, Duration.DAY)
146         if days:
147             result.append("%d d)"%td.days)
148         (hours, left) = divmod (left, Duration.HOUR)
149         if hours:
150             result.append("%d h"%hours)
151         (minutes, seconds) = divmod (left, Duration.MINUTE)
152         if minutes: result.append("%d m"%minutes)
153         if seconds: result.append("%d s"%seconds)
154         if not result: result = ['void']
155         return "-".join(result)
156
157     @staticmethod
158     def validate (duration):
159         # support seconds only for now, works for int/long/str
160         try:
161             return int(duration)
162         except:
163             raise PLCInvalidArgument("Could not parse duration %r"%duration)