def main(): parser = argparse.ArgumentParser() parser.add_argument('-l', '--logfile', nargs=1, metavar='LOGFILE', help='Path to the gtimelog logfile to be use') args = parser.parse_args() if args.logfile is not None: LogFile = set_logfile(args.logfile) else: LogFile = set_logfile() (week_first, week_last) = get_time() Log = TimeLog(LogFile, virtual_midnight) log_entries = Log.window_for(week_first, week_last) total_work, _ = log_entries.totals() _, totals = log_entries.categorized_work_entries() ordered_by_time = [(time, cat) for cat, time in totals.items()] ordered_by_time.sort(reverse=True) max_cat_length = max([len(cat) for cat in totals.keys()]) line_format = ' %-' + str(max_cat_length + 4) + 's %+5s\t %.0f%%' print("\nTotal work done so far : %s\n" % format_duration_short(total_work)) print('Categories by time spent:') for time, cat in ordered_by_time: print(line_format % (cat, format_duration_short(time), time / total_work * 100))
def test_append_adds_blank_line_on_new_day(self): timelog = TimeLog(self.tempfile(), datetime.time(2, 0)) timelog.append('working on sth', now=datetime.datetime(2014, 11, 12, 18, 0)) timelog.append('new day **', now=datetime.datetime(2014, 11, 13, 8, 0)) with open(timelog.filename, 'r') as f: self.assertEqual(f.readlines(), ['2014-11-12 18:00: working on sth\n', '\n', '2014-11-13 08:00: new day **\n'])
def test_appending_clears_window_cache(self): # Regression test for https://github.com/gtimelog/gtimelog/issues/28 timelog = TimeLog(self.tempfile(), datetime.time(2, 0)) w = timelog.window_for_day(datetime.date(2014, 11, 12)) self.assertEqual(list(w.all_entries()), []) timelog.append('started **', now=datetime.datetime(2014, 11, 12, 10, 00)) w = timelog.window_for_day(datetime.date(2014, 11, 12)) self.assertEqual(len(list(w.all_entries())), 1)
def __init__(self, config): self.settings = Settings() self.timelog = TimeLog(self.settings.get_timelog_file(), self.settings.virtual_midnight) self.aliases = config.get('aliases', {}) self.line_format = config.get('line_format', '') if self.line_format == 'categorized': self.line_format_str = "category: task description | comment" self.delimiter = " " else: self.line_format_str = "task: description | comment" self.delimiter = ":"
def window(self, min_dt, max_dt): tl = TimeLog(self.filename, self.virtual_midnight) return TimeWindow( tl, min_dt, max_dt, )
def task_summary(category, task=None): total_delta = datetime.timedelta() today = datetime.datetime.today() epoch = datetime.datetime(1970, 10, 1) Log = TimeLog(LogFile, virtual_midnight) log_entries = Log.window_for(epoch, today) entries, _ = log_entries.categorized_work_entries() for (_, entry, entry_time) in entries[Categories[category] + ' ']: if entry.lstrip() == task: total_delta += entry_time print("total time spent on %s : %d.%d" % ( task, int(total_delta.seconds / 3600) + int(total_delta.days * 24), int(total_delta.seconds / 60 % 60))) return 0
class GtimelogParser(object): def __init__(self, config): self.settings = Settings() self.timelog = TimeLog(self.settings.get_timelog_file(), self.settings.virtual_midnight) self.aliases = config.get('aliases', {}) def skip_entry(self, entry): if '**' in entry: return True if entry.strip() in ('arrive', 'arrived', 'start'): return True return False def get_entries(self, date_window): window = self.timelog.window_for(date_window.start, date_window.stop) worklogs = [] attendances = [] for start, stop, duration, tags, entry in window.all_entries(): if self.skip_entry(entry): continue if attendances and attendances[-1][1] == start: attendances[-1] = (attendances[-1][0], stop) else: attendances += [(start, stop)] try: issue, description = [ x.strip() for x in entry.split(':', 1) if x.strip() ] except ValueError: print( 'Entry must be in the format `task: description`. ' 'Got ', entry) continue # no matter what we find as `issue`: # if we have an alias override it takes precedence if issue in self.aliases: issue = self.aliases[issue] worklogs.append( MultiLog(None, issue, int(duration.total_seconds()), start.date(), description)) # Dangling attendance for today if attendances and attendances[-1][1].date() == date.today(): attendances[-1] = (attendances[-1][0], None) return attendances, worklogs
def test_reloading(self): logfile = self.tempfile() timelog = TimeLog(logfile, datetime.time(2, 0)) # No file - nothing to reload self.assertFalse(timelog.check_reload()) # Create a file - it should be reloaded, once. open(logfile, 'w').close() self.assertTrue(timelog.check_reload()) self.assertFalse(timelog.check_reload()) # Change the timestamp, somehow st = os.stat(logfile) os.utime(logfile, (st.st_atime, st.st_mtime + 1)) self.assertTrue(timelog.check_reload()) self.assertFalse(timelog.check_reload()) # Disappearance of the file is noticed os.unlink(logfile) self.assertTrue(timelog.check_reload()) self.assertFalse(timelog.check_reload())
def test_appending_clears_window_cache(self): # Regression test for https://github.com/gtimelog/gtimelog/issues/28 tempfile = os.path.join(self.mkdtemp(), 'timelog.txt') timelog = TimeLog(tempfile, datetime.time(2, 0)) w = timelog.window_for_day(datetime.date(2014, 11, 12)) self.assertEqual(list(w.all_entries()), []) timelog.append('started **', now=datetime.datetime(2014, 11, 12, 10, 00)) w = timelog.window_for_day(datetime.date(2014, 11, 12)) self.assertEqual(len(list(w.all_entries())), 1)
def test_parse_correction_leaves_regular_text_alone(self): timelog = TimeLog(StringIO(), datetime.time(2, 0)) self.assertEqual(timelog.parse_correction("did stuff"), ("did stuff", None))
def test_parse_correction_handles_virtual_midnight_yesterdays_time(self): # Regression test for https://github.com/gtimelog/gtimelog/issues/33 timelog = TimeLog(StringIO(), datetime.time(2, 0)) self.assertEqual(timelog.parse_correction("15:20 did stuff"), ("did stuff", datetime.datetime(2015, 5, 12, 15, 20)))
def test_valid_time_rejects_times_before_last_entry(self): timelog = TimeLog(StringIO("2015-05-12 15:00: did stuff"), datetime.time(2, 0)) past = datetime.datetime(2015, 5, 12, 14, 20) self.assertFalse(timelog.valid_time(past))
def test_append_rounds_the_time(self): timelog = TimeLog(self.tempfile(), datetime.time(2, 0)) timelog.append('now') self.assertEqual(timelog.items[-1][0], datetime.datetime(2015, 5, 12, 16, 27))
def test_parse_correction_ignores_bad_relative_times(self): timelog = TimeLog(StringIO(), datetime.time(2, 0)) self.assertEqual(timelog.parse_correction("-200 did stuff"), ("-200 did stuff", None))
def test_valid_time_accepts_any_time_in_the_past_when_log_is_empty(self): timelog = TimeLog(StringIO(), datetime.time(2, 0)) past = datetime.datetime(2015, 5, 12, 14, 20) self.assertTrue(timelog.valid_time(past))
def main(): parser = argparse.ArgumentParser() parser.add_argument('-l', '--logfile', nargs=1, metavar='LOGFILE', help='Path to the gtimelog logfile to be use') parser.add_argument('-u', '--user', nargs=1, metavar='USER', help='User Identification to be used for report') parser.add_argument('-n', '--no-time', help='Print weekly report without spent time', action='store_true') parser.add_argument('-m', '--minutes', help='Print weekly report with spent time in minutes', action='store_true') parser.add_argument('-d', '--days', help='Print weekly report with spent time in days', action='store_true') parser.add_argument('-b', '--back', help='Print weekly report back # of weeks', nargs=1) args = parser.parse_args() if args.logfile is not None: LogFile = set_logfile(args.logfile) else: LogFile = set_logfile() if args.user is not None: UserId = set_userid(args.user) else: UserId = set_userid() (week_first, week_last) = get_time(args.back) Log = TimeLog(LogFile, virtual_midnight) log_entries = Log.window_for(week_first, week_last) total_work, _ = log_entries.totals() entries, totals = log_entries.categorized_work_entries() print("[ACTIVITY] %s to %s (%s)" % (week_first.isoformat().split("T")[0], week_last.isoformat().split("T")[0], UserId)) if entries: if None in entries: categories = sorted(entries) categories.append('No category') entries['No category'] = e t = totals.pop(None) totals['No category'] = t else: categories = sorted(entries) for cat in categories: print('%s:' % cat) work = [(entry, duration) for start, entry, duration in entries[cat]] work.sort() for entry, duration in work: if not duration: continue # skip empty "arrival" entries entry = entry[:1].upper() + entry[1:] if args.no_time: print(u" %-61s " % entry) elif args.minutes: print(u" %-85s %+5s %+4s" % (entry, format_duration_short(duration), as_minutes(duration))) elif args.days: print(u" %-85s %+5s %.1f" % (entry, format_duration_short(duration), as_minutes(duration) / 480)) else: print(u" %-85s %+5s" % (entry, format_duration_short(duration))) if args.no_time: print("") else: if args.minutes: print('-' * 100) print(u"%+94s %4s" % (format_duration_short( totals[cat]), as_minutes(totals[cat]))) elif args.days: print('-' * 98) print(u"%+94s %.1f" % (format_duration_short( totals[cat]), as_minutes(totals[cat]) / 480)) else: print('-' * 94) print(u"%+94s" % (format_duration_short(totals[cat]))) print("Total work done : %s" % format_duration_short(total_work))
def test_parse_correction_recognizes_relative_times(self): timelog = TimeLog(StringIO(), datetime.time(2, 0)) self.assertEqual(timelog.parse_correction("-20 did stuff"), ("did stuff", datetime.datetime(2015, 5, 12, 16, 7)))
def do_activate(self): if self.main_window is not None: self.main_window.main_window.present() return log.addHandler(logging.StreamHandler(sys.stdout)) if self.opts.debug: log.setLevel(logging.DEBUG) else: log.setLevel(logging.INFO) if self.opts.sample_config: settings = Settings() settings.save("gtimelogrc.sample") print("Sample configuration file written to gtimelogrc.sample") print("Edit it and save as %s" % settings.get_config_file()) return if self.opts.debug: print('GTimeLog version: %s' % gtimelog.__version__) print('Gtk+ version: %s.%s.%s' % (Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION, Gtk.MICRO_VERSION)) print('Config directory: %s' % Settings().get_config_dir()) print('Data directory: %s' % Settings().get_data_dir()) settings = Settings() configdir = settings.get_config_dir() datadir = settings.get_data_dir() try: # Create configdir if it doesn't exist. os.makedirs(configdir) except OSError as error: if error.errno != errno.EEXIST: # XXX: not the most friendly way of error reporting for a GUI app raise try: # Create datadir if it doesn't exist. os.makedirs(datadir) except OSError as error: if error.errno != errno.EEXIST: raise settings_file = settings.get_config_file() if not os.path.exists(settings_file): if self.opts.debug: print('Saving settings to %s' % settings_file) settings.save(settings_file) else: if self.opts.debug: print('Loading settings from %s' % settings_file) settings.load(settings_file) if self.opts.debug: print('Assuming date changes at %s' % settings.virtual_midnight) print('Loading time log from %s' % settings.get_timelog_file()) timelog = TimeLog(settings.get_timelog_file(), settings.virtual_midnight) if settings.task_list_url: if self.opts.debug: print('Loading cached remote tasks from %s' % os.path.join(datadir, 'remote-tasks.txt')) tasks = RemoteTaskList(settings.task_list_url, os.path.join(datadir, 'remote-tasks.txt')) else: if self.opts.debug: print('Loading tasks from %s' % os.path.join(datadir, 'tasks.txt')) tasks = TaskList(os.path.join(datadir, 'tasks.txt')) self.main_window = MainWindow(timelog, settings, tasks) self.add_window(self.main_window.main_window) start_in_tray = False if settings.show_tray_icon: if self.opts.debug: print('Tray icon preference: %s' % ('AppIndicator' if settings.prefer_app_indicator else 'SimpleStatusIcon')) if settings.prefer_app_indicator and have_app_indicator: tray_icon = AppIndicator(self.main_window) else: tray_icon = SimpleStatusIcon(self.main_window) if tray_icon: if self.opts.debug: print('Using: %s' % tray_icon.__class__.__name__) start_in_tray = (settings.start_in_tray if settings.start_in_tray else self.opts.tray) if not start_in_tray: self.main_window.on_show_activate() else: if self.opts.debug: print('Starting minimized') # This is needed to make ^C terminate gtimelog when we're using # gobject-introspection. signal.signal(signal.SIGINT, signal.SIG_DFL)
def full(): return TimeLog(Settings().get_timelog_file(), Settings().virtual_midnight).items
def test_window_for_month(self): timelog = TimeLog(StringIO(), datetime.time(2, 0)) for d in range(1, 31): window = timelog.window_for_month(datetime.date(2015, 9, d)) self.assertEqual(window.min_timestamp, datetime.datetime(2015, 9, 1, 2, 0)) self.assertEqual(window.max_timestamp, datetime.datetime(2015, 10, 1, 2, 0))
def test_window_for_date_range(self): timelog = TimeLog(StringIO(), datetime.time(2, 0)) window = timelog.window_for_date_range(datetime.date(2015, 9, 3), datetime.date(2015, 9, 24)) self.assertEqual(window.min_timestamp, datetime.datetime(2015, 9, 3, 2, 0)) self.assertEqual(window.max_timestamp, datetime.datetime(2015, 9, 25, 2, 0))
def __init__(self, config): self.settings = Settings() self.timelog = TimeLog(self.settings.get_timelog_file(), self.settings.virtual_midnight) self.aliases = config.get('aliases', {})
def test_parse_correction_handles_virtual_midnight_todays_time(self): timelog = TimeLog(StringIO(), datetime.time(2, 0)) self.assertEqual(timelog.parse_correction("00:15 did stuff"), ("did stuff", datetime.datetime(2015, 5, 13, 00, 15)))
def test_parse_correction_ignores_bad_absolute_times(self): timelog = TimeLog(StringIO(), datetime.time(2, 0)) self.assertEqual(timelog.parse_correction("19:60 did stuff"), ("19:60 did stuff", None)) self.assertEqual(timelog.parse_correction("24:00 did stuff"), ("24:00 did stuff", None))
def test_parse_correction_ignores_relative_times_before_last_entry(self): timelog = TimeLog(StringIO("2015-05-12 16:00: stuff"), datetime.time(2, 0)) self.assertEqual(timelog.parse_correction("-30 did stuff"), ("-30 did stuff", None))
def make_time_window(file=None, min=None, max=None, vm=datetime.time(2)): from gtimelog.timelog import TimeLog if file is None: file = StringIO() return TimeLog(file, vm).window_for(min, max)
def test_valid_time_rejects_times_in_the_future(self): timelog = TimeLog(StringIO(), datetime.time(2, 0)) future = datetime.datetime(2015, 5, 12, 16, 30) self.assertFalse(timelog.valid_time(future))
def test_window_for_day(self): timelog = TimeLog(StringIO(), datetime.time(2, 0)) window = timelog.window_for_day(datetime.date(2015, 9, 17)) self.assertEqual(window.min_timestamp, datetime.datetime(2015, 9, 17, 2, 0)) self.assertEqual(window.max_timestamp, datetime.datetime(2015, 9, 18, 2, 0))
def test_valid_time_accepts_times_between_last_entry_and_now(self): timelog = TimeLog(StringIO("2015-05-12 15:00: did stuff"), datetime.time(2, 0)) past = datetime.datetime(2015, 5, 12, 15, 20) self.assertTrue(timelog.valid_time(past))
def main(): parser = argparse.ArgumentParser() parser.add_argument('-l', '--logfile', nargs=1, metavar='LOGFILE', help='Path to the gtimelog logfile to be use') parser.add_argument('-u', '--user', nargs=1, metavar='USER', help='User Identification to be used for report') parser.add_argument('-t', '--time', help='Print daily report with spent time', action='store_false') parser.add_argument('-H', '--header', help='Print category headers', action='store_true') parser.add_argument('-m', '--minutes', help='Print daily report with spent time in minutes', action='store_true') parser.add_argument('-b', '--back', help='Print daily report back # of days', metavar='BACK', type=int) args = parser.parse_args() if args.logfile is not None: LogFile = set_logfile(args.logfile) else: LogFile = set_logfile() if args.user is not None: UserId = set_userid(args.user) else: UserId = set_userid() day_start = datetime.datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) if args.back: day_start = day_start - datetime.timedelta(days=args.back) day_end = day_start.replace(hour=23, minute=59, second=59, microsecond=999) Log = TimeLog(LogFile, virtual_midnight) log_entries = Log.window_for(day_start, day_end) total_work, _ = log_entries.totals() entries, totals = log_entries.categorized_work_entries() print("Today's work for %s" % UserId) if entries: if None in entries: categories = sorted(entries) categories.append('No category') entries['No category'] = e t = totals.pop(None) totals['No category'] = t else: categories = sorted(entries) for cat in categories: if args.header: print('- %s:' % cat) work = [(entry, duration) for start, entry, duration in entries[cat]] work.sort() for entry, duration in work: if not duration: continue # skip empty "arrival" entries entry = entry[:1].upper() + entry[1:] if args.time: print(u" - %-61s " % entry) else: if args.minutes: print(u" - %-59s %+5s %+4s" % (entry, format_duration_short(duration), as_minutes(duration))) else: print(u" - %-59s %+5s" % (entry, format_duration_short(duration))) if args.time: pass # print("") else: if args.minutes: print('-' * 75) print(u"%+70s %4s" % (format_duration_short( totals[cat]), as_minutes(totals[cat]))) else: print('-' * 70) print(u"%+70s" % (format_duration_short(totals[cat]))) print("Total work done : %s" % format_duration_short(total_work))
def get_time_log(self): return TimeLog(self.get_timelog_file(), self.virtual_midnight)