import json import re import pygal from datetime import datetime, timedelta from os.path import expanduser, exists from collections import OrderedDict STAMPS_FILE = expanduser('~/.workstamps.json') DATE_FORMAT = '%Y-%m-%d' DATETIME_FORMAT = '%Y-%m-%d %H:%M' HOURS_DAY = 8 SECS_DAY = HOURS_DAY * 60 * 60 class Stamper(object): def __init__(self, stamps_file=STAMPS_FILE): self.stamps_file = STAMPS_FILE self.ensure_stamps_file() self.stamps = [] def ensure_stamps_file(self): if not exists(self.stamps_file): with open(self.stamps_file, 'w') as stamps_file: stamps_file.write('') def load_stamps(self): with open(self.stamps_file, 'r') as stamps_file: try: self.stamps = json.load(stamps_file) except ValueError: self.stamps = [] def save_stamps(self): with open(self.stamps_file, 'w') as stamps_file: json.dump(self.stamps, stamps_file, indent=4) def stamp(self, start, end, customer, action): self.stamps.append({ 'start': start, 'end': end, 'customer': customer, 'action': action, }) def last_stamp(self): if not self.stamps: return None return self.stamps[-1] def worktime(self, start, end): worktime = (datetime.strptime(end, DATETIME_FORMAT) - datetime.strptime(start, DATETIME_FORMAT)) return worktime.seconds def validate_filter(self, stamp_filter): """ Validate a given filter. Filters can have the following notation: - %Y-%m-%d--%Y-%m-%d: Times recorded between two dates - _%Y-%m-%d: Times recorded before a given date - +%Y-%m-%d: Times recorded after a given date - N...N[d|w|m|y]: Times recorded N...N days/weeks/months/years ago """ # First try the date filters, one by one matches = ['%Y-%m-%d', '_%Y-%m-%d', '+%Y-%m-%d'] for match in matches: try: if '--' in stamp_filter: filter_from, filter_to = stamp_filter.split('--') filter_from = datetime.strptime(filter_from, match) filter_to = datetime.strptime(filter_to, match) else: valid_filter = datetime.strptime(stamp_filter, match) except ValueError: pass else: return stamp_filter valid_filter = re.search(r'(\d+[dwmyDWMY]{1})', stamp_filter) if valid_filter: return stamp_filter # Not a valid filter return None def customers(self): customers = [] for stamp in self.stamps: if stamp['customer'] not in customers: customers.append(stamp['customer']) customers.remove(None) return customers def totals(self, stamp_filter=None): totals = {} for stamp in self.stamps: customer = stamp['customer'] if customer: # c will be None for "start" stamps, having no end time if customer not in totals: totals[customer] = 0 totals[customer] += self.worktime(stamp['start'], stamp['end']) return totals def details(self): details = OrderedDict() totals = OrderedDict() total_customer = OrderedDict() for stamp in self.stamps: if stamp['customer']: # avoid "start" stamps start_day = stamp['start'].split(' ')[0] if start_day not in details: details[start_day] = [] if start_day not in totals: totals[start_day] = 0 worktime = self.worktime(stamp['start'], stamp['end']) details[start_day].append( ' -> %(worktime)s %(customer)s %(action)s' % { 'worktime': str(timedelta(seconds=worktime)), 'customer': stamp['customer'], 'action': stamp['action'] }) customer = stamp['customer'] totals[start_day] += worktime if start_day not in total_customer: total_customer[start_day] = {} if customer not in total_customer[start_day]: total_customer[start_day][customer] = 0 total_customer[start_day][customer] += worktime for day in totals: totals[day] = str(timedelta(seconds=totals[day])) return details, totals, total_customer def details_by_customer(self, customer): details = OrderedDict() totals = OrderedDict() for stamp in self.stamps: if stamp['customer'] == customer: start_day = stamp['start'].split(' ')[0] if start_day not in details: details[start_day] = [] if start_day not in totals: totals[start_day] = 0 worktime = self.worktime(stamp['start'], stamp['end']) details[start_day].append( ' -> %(worktime)s %(customer)s %(action)s' % { 'worktime': str(timedelta(seconds=worktime)), 'customer': stamp['customer'], 'action': stamp['action'] }) totals[start_day] += worktime for day in totals: totals[day] = str(timedelta(seconds=totals[day])) return details, totals def show_stamps(self, customer=None, stamp_filter=None, verbose=False, sum=False, graph=False): if stamp_filter: stamp_filter = self.validate_filter(stamp_filter) totals = self.totals(stamp_filter) if customer: seconds=totals.get(customer, 0) total = timedelta(seconds=totals.get(customer, 0)) print(' %(customer)s: %(total)s' % {'customer': customer, 'total': total}) else: for c in totals: seconds=totals[c] total = timedelta(seconds=totals[c]) print(' %(customer)s: %(total)s' % {'customer': c, 'total': total}) if verbose: if customer: details, totals = self.details_by_customer(customer) else: details, totals, total_customer = self.details() for day in details: print('------ %(day)s ------' % {'day': day}) for line in details[day]: print(line) print(' Total: %(total)s' % {'total': totals[day]}) if sum: sum_tot = '' if totals: print('------ Totals ------' % {'day': day}) for day, tot in totals.iteritems(): print(' %(day)s: %(total)s' % {'day': day, 'total': tot}) sum_tot = "%(total)s %(new)s" % { 'total': sum_tot, 'new': total } totalSecs, sec = divmod(seconds, 60) hr, min = divmod(totalSecs, 60) totalDays, remaining = divmod(seconds, SECS_DAY) remainingMin, remainingSec = divmod(remaining, (60)) remainingHr, remainingMin = divmod(remainingMin, (60)) print('----- %d:%02d:%02d -----' % (hr, min, sec)) print('--- %d days, remaining: %d:%02d (%d hours/day) ---' % ( totalDays, remainingHr, remainingMin, HOURS_DAY )) if graph: DAYS = 15 list_days = [] list_tot = [] stackedbar_chart = pygal.StackedBar() stackedbar_chart.title = 'Worked time per day (in hours)' if customer: for day, tot in totals.iteritems(): list_days.append(day) (h, m, s) = tot.split(':') tot_sec = int(h) * 3600 + int(m) * 60 + int(s) tot_h = float(tot_sec / float(60) / float(60)) list_tot.append(tot_h) stackedbar_chart.add(customer, list_tot) stackedbar_chart.x_labels = map(str, list_days) stackedbar_chart.render_to_file('graphs/chart-%s.svg' % customer ) else: all_customers = self.customers() total_per_customer = {} details, totals, total_customer = self.details() chars = 0 total_customer_reverse = total_customer.items() total_customer_reverse.reverse() for day, tot in total_customer_reverse: if chars < DAYS: list_days.append(day) for cust in self.customers(): if cust not in tot: tot[cust] = 0 for cus, time in tot.iteritems(): tot_h = float(time / float(60) / float(60)) if cus not in total_per_customer: total_per_customer[cus] = [] total_per_customer[cus].append(tot_h) chars = chars + 1 for ccus, ctime in total_per_customer.iteritems(): stackedbar_chart.add(ccus, ctime) stackedbar_chart.x_labels = map(str, list_days[-DAYS:]) stackedbar_chart.render_to_file('graphs/chart-all.svg')