source: stamper/stamper/stamper.py@ 25:8e33103a7e36

Last change on this file since 25:8e33103a7e36 was 25:8e33103a7e36, checked in by Borja Lopez <borja@…>, 10 years ago

Set two _FORMAT variables for DATE and DATETIME objects.
This naming is more consistent, now that we use both date and
datetime objects.

File size: 9.6 KB
Line 
1
2import json
3import re
4import pygal
5from datetime import datetime, 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--%Y-%m-%d: Times recorded between two dates
63
64 - _%Y-%m-%d: Times recorded before a given date
65
66 - +%Y-%m-%d: Times recorded after a given date
67
68 - N...N[d|w|m|y]: Times recorded N...N days/weeks/months/years ago
69 """
70 # First try the date filters, one by one
71 matches = ['%Y-%m-%d', '_%Y-%m-%d', '+%Y-%m-%d']
72 for match in matches:
73 try:
74 if '--' in stamp_filter:
75 filter_from, filter_to = stamp_filter.split('--')
76 filter_from = datetime.strptime(filter_from, match)
77 filter_to = datetime.strptime(filter_to, match)
78 else:
79 valid_filter = datetime.strptime(stamp_filter, match)
80 except ValueError:
81 pass
82 else:
83 return stamp_filter
84
85 valid_filter = re.search(r'(\d+[dwmyDWMY]{1})', stamp_filter)
86 if valid_filter:
87 return stamp_filter
88
89 # Not a valid filter
90 return None
91
92 def customers(self):
93 customers = []
94 for stamp in self.stamps:
95 if stamp['customer'] not in customers:
96 customers.append(stamp['customer'])
97 customers.remove(None)
98 return customers
99
100 def totals(self, stamp_filter=None):
101 totals = {}
102 for stamp in self.stamps:
103 customer = stamp['customer']
104 if customer:
105 # c will be None for "start" stamps, having no end time
106 if customer not in totals:
107 totals[customer] = 0
108 totals[customer] += self.worktime(stamp['start'], stamp['end'])
109 return totals
110
111 def details(self):
112 details = OrderedDict()
113 totals = OrderedDict()
114 total_customer = OrderedDict()
115 for stamp in self.stamps:
116 if stamp['customer']:
117 # avoid "start" stamps
118 start_day = stamp['start'].split(' ')[0]
119 if start_day not in details:
120 details[start_day] = []
121 if start_day not in totals:
122 totals[start_day] = 0
123 worktime = self.worktime(stamp['start'], stamp['end'])
124 details[start_day].append(
125 ' -> %(worktime)s %(customer)s %(action)s' % {
126 'worktime': str(timedelta(seconds=worktime)),
127 'customer': stamp['customer'],
128 'action': stamp['action']
129 })
130 customer = stamp['customer']
131 totals[start_day] += worktime
132 if start_day not in total_customer:
133 total_customer[start_day] = {}
134 if customer not in total_customer[start_day]:
135 total_customer[start_day][customer] = 0
136 total_customer[start_day][customer] += worktime
137 for day in totals:
138 totals[day] = str(timedelta(seconds=totals[day]))
139 return details, totals, total_customer
140
141 def details_by_customer(self, customer):
142 details = OrderedDict()
143 totals = OrderedDict()
144 for stamp in self.stamps:
145 if stamp['customer'] == customer:
146 start_day = stamp['start'].split(' ')[0]
147 if start_day not in details:
148 details[start_day] = []
149 if start_day not in totals:
150 totals[start_day] = 0
151 worktime = self.worktime(stamp['start'], stamp['end'])
152 details[start_day].append(
153 ' -> %(worktime)s %(customer)s %(action)s' % {
154 'worktime': str(timedelta(seconds=worktime)),
155 'customer': stamp['customer'],
156 'action': stamp['action']
157 })
158 totals[start_day] += worktime
159 for day in totals:
160 totals[day] = str(timedelta(seconds=totals[day]))
161 return details, totals
162
163 def show_stamps(self, customer=None, stamp_filter=None, verbose=False,
164 sum=False, graph=False):
165 if stamp_filter:
166 stamp_filter = self.validate_filter(stamp_filter)
167
168 totals = self.totals(stamp_filter)
169
170 if customer:
171 seconds=totals.get(customer, 0)
172 total = timedelta(seconds=totals.get(customer, 0))
173 print(' %(customer)s: %(total)s' % {'customer': customer,
174 'total': total})
175 else:
176 for c in totals:
177 seconds=totals[c]
178 total = timedelta(seconds=totals[c])
179 print(' %(customer)s: %(total)s' % {'customer': c,
180 'total': total})
181
182 if verbose:
183 if customer:
184 details, totals = self.details_by_customer(customer)
185 else:
186 details, totals, total_customer = self.details()
187 for day in details:
188 print('------ %(day)s ------' % {'day': day})
189 for line in details[day]:
190 print(line)
191 print(' Total: %(total)s' % {'total': totals[day]})
192
193 if sum:
194 sum_tot = ''
195 if totals:
196 print('------ Totals ------' % {'day': day})
197 for day, tot in totals.iteritems():
198 print(' %(day)s: %(total)s' % {'day': day, 'total': tot})
199 sum_tot = "%(total)s %(new)s" % {
200 'total': sum_tot,
201 'new': total
202 }
203 totalSecs, sec = divmod(seconds, 60)
204 hr, min = divmod(totalSecs, 60)
205 totalDays, remaining = divmod(seconds, SECS_DAY)
206 remainingMin, remainingSec = divmod(remaining, (60))
207 remainingHr, remainingMin = divmod(remainingMin, (60))
208 print('----- %d:%02d:%02d -----' % (hr, min, sec))
209 print('--- %d days, remaining: %d:%02d (%d hours/day) ---' % (
210 totalDays, remainingHr, remainingMin, HOURS_DAY
211 ))
212
213 if graph:
214 DAYS = 15
215 list_days = []
216 list_tot = []
217 stackedbar_chart = pygal.StackedBar()
218 stackedbar_chart.title = 'Worked time per day (in hours)'
219
220 if customer:
221 for day, tot in totals.iteritems():
222 list_days.append(day)
223 (h, m, s) = tot.split(':')
224 tot_sec = int(h) * 3600 + int(m) * 60 + int(s)
225 tot_h = float(tot_sec / float(60) / float(60))
226 list_tot.append(tot_h)
227 stackedbar_chart.add(customer, list_tot)
228 stackedbar_chart.x_labels = map(str, list_days)
229 stackedbar_chart.render_to_file('graphs/chart-%s.svg' % customer )
230 else:
231 all_customers = self.customers()
232 total_per_customer = {}
233 details, totals, total_customer = self.details()
234 chars = 0
235 total_customer_reverse = total_customer.items()
236 total_customer_reverse.reverse()
237 for day, tot in total_customer_reverse:
238 if chars < DAYS:
239 list_days.append(day)
240 for cust in self.customers():
241 if cust not in tot:
242 tot[cust] = 0
243 for cus, time in tot.iteritems():
244 tot_h = float(time / float(60) / float(60))
245 if cus not in total_per_customer:
246 total_per_customer[cus] = []
247 total_per_customer[cus].append(tot_h)
248 chars = chars + 1
249 for ccus, ctime in total_per_customer.iteritems():
250 stackedbar_chart.add(ccus, ctime)
251 stackedbar_chart.x_labels = map(str, list_days[-DAYS:])
252 stackedbar_chart.render_to_file('graphs/chart-all.svg')
Note: See TracBrowser for help on using the repository browser.