source: stamper/stamper/stamper.py@ 28:9afaa509f383

Last change on this file since 28:9afaa509f383 was 28:9afaa509f383, checked in by Borja Lopez <borja@…>, 10 years ago

Added the "timeline" feature (stamps -t), which prints on screen a
timeline of the recorded entries, in betabug's stamp "old style".

Useful for two purposes:

1) Export recorded times to be used by betabug's stamp

2) Get a quick view of your latest records (to notice if the last

one was a start, for example

File size: 12.5 KB
Line 
1
2import json
3import re
4import pygal
5from datetime import datetime, date, timedelta
6from os.path import expanduser, exists
7from collections import OrderedDict
8
9
10STAMPS_FILE = expanduser('~/.workstamps.json')
11DATE_FORMAT = '%Y-%m-%d'
12DATETIME_FORMAT = '%Y-%m-%d %H:%M'
13HOURS_DAY = 8
14SECS_DAY = HOURS_DAY * 60 * 60
15
16
17class Stamper(object):
18
19 def __init__(self, stamps_file=STAMPS_FILE):
20 self.stamps_file = STAMPS_FILE
21 self.ensure_stamps_file()
22 self.stamps = []
23
24 def ensure_stamps_file(self):
25 if not exists(self.stamps_file):
26 with open(self.stamps_file, 'w') as stamps_file:
27 stamps_file.write('')
28
29 def load_stamps(self):
30 with open(self.stamps_file, 'r') as stamps_file:
31 try:
32 self.stamps = json.load(stamps_file)
33 except ValueError:
34 self.stamps = []
35
36 def save_stamps(self):
37 with open(self.stamps_file, 'w') as stamps_file:
38 json.dump(self.stamps, stamps_file, indent=4)
39
40 def stamp(self, start, end, customer, action):
41 self.stamps.append({
42 'start': start,
43 'end': end,
44 'customer': customer,
45 'action': action,
46 })
47
48 def last_stamp(self):
49 if not self.stamps:
50 return None
51 return self.stamps[-1]
52
53 def worktime(self, start, end):
54 worktime = (datetime.strptime(end, DATETIME_FORMAT) -
55 datetime.strptime(start, DATETIME_FORMAT))
56 return worktime.seconds
57
58 def validate_filter(self, stamp_filter):
59 """
60 Validate a given filter. Filters can have the following notation:
61
62 - %Y-%m-%d: Times recorded at a given date
63
64 - %Y-%m-%d--%Y-%m-%d: Times recorded between two dates
65
66 - *%Y-%m-%d: Times recorded up to a given date
67
68 - %Y-%m-%d*: Times recorded from a given date
69
70 - N...N[d|w|m|y]: Times recorded N...N days/weeks/months/years ago
71
72 Important: all date comparisons are made on datetime objects, using
73 00:00 as the time (first second of the given day). This means that
74 for range filters, the first day is included, but the second day is not
75 """
76 filter_from = None
77 filter_to = None
78
79 if stamp_filter is None:
80 return filter_from, filter_to
81
82 if '--' in stamp_filter:
83 filter_from, filter_to = stamp_filter.split('--')
84 filter_from = datetime.strptime(filter_from, DATE_FORMAT)
85 filter_to = datetime.strptime(filter_to, DATE_FORMAT)
86
87 elif stamp_filter.startswith('*'):
88 filter_to = datetime.strptime(stamp_filter, '*'+DATE_FORMAT)
89 filter_to = filter_to.replace(hour=0, minute=0, second=0)
90
91 elif stamp_filter.endswith('*'):
92 filter_from = datetime.strptime(stamp_filter, DATE_FORMAT+'*')
93 filter_from = filter_from.replace(hour=0, minute=0, second=0)
94
95 elif re.search(r'(\d+[dD]{1})', stamp_filter):
96 number = int(stamp_filter.lower().replace('d', ''))
97 delta = timedelta(days=number)
98 filter_from = datetime.today() - delta
99 filter_from = filter_from.replace(hour=0, minute=0, second=0)
100
101 elif re.search(r'(\d+[wW]{1})', stamp_filter):
102 number = int(stamp_filter.lower().replace('w', '')) * 7
103 delta = timedelta(days=number)
104 filter_from = datetime.today() - delta
105 filter_from = filter_from.replace(hour=0, minute=0, second=0)
106
107 elif re.search(r'(\d+[mM]{1})', stamp_filter):
108 number = int(stamp_filter.lower().replace('m', ''))
109 past = date.today()
110 # start travelling in time, back to N months ago
111 for n in range(number):
112 past = past.replace(day=1) - timedelta(days=1)
113 # Now use the year/month from the past + the current day to set
114 # the proper date
115 filter_from = datetime(past.year, past.month, date.today().day)
116
117 elif re.search(r'(\d+[yY]{1})', stamp_filter):
118 number = int(stamp_filter.lower().replace('y', ''))
119 today = date.today()
120 filter_from = datetime(today.year - number, today.month, today.day)
121
122 else:
123 # maybe they are giving us a fixed date
124 filter_from = datetime.strptime(stamp_filter, DATE_FORMAT)
125 filter_from = filter_from.replace(hour=0, minute=0, second=0)
126 filter_to = filter_from + timedelta(days=1)
127
128 return filter_from, filter_to
129
130 def customers(self):
131 customers = []
132 for stamp in self.stamps:
133 if stamp['customer'] not in customers:
134 customers.append(stamp['customer'])
135 customers.remove(None)
136 return customers
137
138 def totals(self, filter_from=None, filter_to=None):
139 totals = {}
140 for stamp in self.stamps:
141 customer = stamp['customer']
142 # customer will be None for "start" stamps, having no end time
143 if customer:
144 start = datetime.strptime(stamp['start'], DATETIME_FORMAT)
145 end = datetime.strptime(stamp['end'], DATETIME_FORMAT)
146 if filter_from and start < filter_from:
147 # if there is a filter setting a starting date for the
148 # report and the current stamp is from an earlier date, do
149 # not add it to the totals
150 continue
151 if filter_to and start > filter_to:
152 # similar for the end date
153 continue
154 if customer not in totals:
155 totals[customer] = 0
156 totals[customer] += self.worktime(stamp['start'], stamp['end'])
157 return totals
158
159 def details(self, filter_customer=None, filter_from=None, filter_to=None):
160 details = OrderedDict()
161 totals = OrderedDict()
162 total_customer = OrderedDict()
163 for stamp in self.stamps:
164 customer = stamp['customer']
165 if customer:
166 if filter_customer and customer != filter_customer:
167 # we are getting the details for only one customer, if this
168 # stamp is not for that customer, simply move on and ignore
169 # it
170 continue
171 start = datetime.strptime(stamp['start'], DATETIME_FORMAT)
172 start_day = start.strftime('%Y-%m-%d')
173 end = datetime.strptime(stamp['end'], DATETIME_FORMAT)
174 if filter_from and start < filter_from:
175 # if there is a filter setting a starting date for the
176 # report and the current stamp is from an earlier date, do
177 # not add it to the totals
178 continue
179 if filter_to and start > filter_to:
180 # similar for the end date
181 continue
182 # avoid "start" stamps
183 if start_day not in details:
184 details[start_day] = []
185 if start_day not in totals:
186 totals[start_day] = 0
187 worktime = self.worktime(stamp['start'], stamp['end'])
188 details[start_day].append(
189 ' -> %(worktime)s %(customer)s %(action)s' % {
190 'worktime': str(timedelta(seconds=worktime)),
191 'customer': customer,
192 'action': stamp['action']
193 })
194 totals[start_day] += worktime
195 if start_day not in total_customer:
196 total_customer[start_day] = {}
197 if customer not in total_customer[start_day]:
198 total_customer[start_day][customer] = 0
199 total_customer[start_day][customer] += worktime
200 for day in totals:
201 totals[day] = str(timedelta(seconds=totals[day]))
202 return details, totals, total_customer
203
204 def timeline(self):
205 for stamp in self.stamps:
206 customer = stamp['customer']
207 if not stamp['customer']:
208 print(stamp['start'] + ' start')
209 else:
210 print(' '.join([stamp['end'],
211 stamp['customer'],
212 stamp['action']]))
213
214 def show_stamps(self, customer=None, stamp_filter=None, verbose=False,
215 sum=False, graph=False):
216
217 filter_from, filter_to = self.validate_filter(stamp_filter)
218
219 # If the user asks for verbose information, show it before the
220 # totals (mimicing what the original stamp tool does)
221 if verbose:
222 details, totals, total_customer = self.details(customer,
223 filter_from,
224 filter_to)
225 for day in details:
226 print('------ %(day)s ------' % {'day': day})
227 for line in details[day]:
228 print(line)
229 print(' Total: %(total)s' % {'total': totals[day]})
230 print '-'*79
231
232 # now calculate the totals and show them
233 totals = self.totals(filter_from, filter_to)
234 if customer:
235 seconds=totals.get(customer, 0)
236 total = timedelta(seconds=totals.get(customer, 0))
237 print(' %(customer)s: %(total)s' % {'customer': customer,
238 'total': total})
239 else:
240 for c in totals:
241 seconds=totals[c]
242 total = timedelta(seconds=totals[c])
243 print(' %(customer)s: %(total)s' % {'customer': c,
244 'total': total})
245
246 if sum:
247 sum_tot = ''
248 if totals:
249 print('------ Totals ------' % {'day': day})
250 for day, tot in totals.iteritems():
251 print(' %(day)s: %(total)s' % {'day': day, 'total': tot})
252 sum_tot = "%(total)s %(new)s" % {
253 'total': sum_tot,
254 'new': total
255 }
256 totalSecs, sec = divmod(seconds, 60)
257 hr, min = divmod(totalSecs, 60)
258 totalDays, remaining = divmod(seconds, SECS_DAY)
259 remainingMin, remainingSec = divmod(remaining, (60))
260 remainingHr, remainingMin = divmod(remainingMin, (60))
261 print('----- %d:%02d:%02d -----' % (hr, min, sec))
262 print('--- %d days, remaining: %d:%02d (%d hours/day) ---' % (
263 totalDays, remainingHr, remainingMin, HOURS_DAY
264 ))
265
266 if graph:
267 DAYS = 15
268 list_days = []
269 list_tot = []
270 stackedbar_chart = pygal.StackedBar()
271 stackedbar_chart.title = 'Worked time per day (in hours)'
272
273 if customer:
274 for day, tot in totals.iteritems():
275 list_days.append(day)
276 (h, m, s) = tot.split(':')
277 tot_sec = int(h) * 3600 + int(m) * 60 + int(s)
278 tot_h = float(tot_sec / float(60) / float(60))
279 list_tot.append(tot_h)
280 stackedbar_chart.add(customer, list_tot)
281 stackedbar_chart.x_labels = map(str, list_days)
282 stackedbar_chart.render_to_file('graphs/chart-%s.svg' % customer )
283 else:
284 all_customers = self.customers()
285 total_per_customer = {}
286 details, totals, total_customer = self.details()
287 chars = 0
288 total_customer_reverse = total_customer.items()
289 total_customer_reverse.reverse()
290 for day, tot in total_customer_reverse:
291 if chars < DAYS:
292 list_days.append(day)
293 for cust in self.customers():
294 if cust not in tot:
295 tot[cust] = 0
296 for cus, time in tot.iteritems():
297 tot_h = float(time / float(60) / float(60))
298 if cus not in total_per_customer:
299 total_per_customer[cus] = []
300 total_per_customer[cus].append(tot_h)
301 chars = chars + 1
302 for ccus, ctime in total_per_customer.iteritems():
303 stackedbar_chart.add(ccus, ctime)
304 stackedbar_chart.x_labels = map(str, list_days[-DAYS:])
305 stackedbar_chart.render_to_file('graphs/chart-all.svg')
Note: See TracBrowser for help on using the repository browser.