From a7c33c7e9fdc1cb77dfa81883f87fd93205b98b0 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Mon, 8 Oct 2018 10:31:55 +0200 Subject: [PATCH 01/19] adds functionalty to show log for a special day or day. --- ti/__init__.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index 89f00bf..9e13925 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -10,7 +10,7 @@ ti (s|status) ti (t|tag) ... ti (n|note) ... - ti (l|log) [today] + ti (l|log) [today|yyyy-mm-dd] ti (e|edit) ti (i|interrupt) ti --no-color @@ -247,9 +247,22 @@ def action_log(period): work = data['work'] + data['interrupt_stack'] log = defaultdict(lambda: {'delta': timedelta()}) current = None - + comparedate = None + if period: + if period == "today": + comparedate = datetime.today() + else: + try: + comparedate = datetime.strptime(period, "%Y-%m-%d") + except ValueError: + print("Please use dateformat yyyy-mm-dd") for item in work: start_time = parse_isotime(item['start']) + if (period and (start_time.year != comparedate.year or + start_time.month != comparedate.month or + start_time.day != comparedate.day)): + continue + if 'end' in item: log[item['name']]['delta'] += ( parse_isotime(item['end']) - start_time) From d0294c074b839a392cce46d0ff5bdb2530e3a36f Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Mon, 8 Oct 2018 10:45:05 +0200 Subject: [PATCH 02/19] adds shebang --- ti/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ti/__init__.py b/ti/__init__.py index 9e13925..7ade6b1 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # coding: utf-8 """ From 83f0de612ab7041f109071d3eb4c7021476136f0 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Sat, 20 Oct 2018 10:25:03 +0200 Subject: [PATCH 03/19] using local time as it makes more sence for me --- ti/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index 7ade6b1..a57d275 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -347,7 +347,7 @@ def to_datetime(timestr): def parse_engtime(timestr): - now = datetime.utcnow() + now = datetime.now() if not timestr or timestr.strip() == 'now': return now @@ -403,7 +403,6 @@ def timegap(start_time, end_time): else: return 'more than a year' - def parse_args(argv=sys.argv): global use_color From e9762016ad71b93907b58e0f76a108879a1fcb15 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Tue, 23 Oct 2018 09:16:40 +0200 Subject: [PATCH 04/19] replaces utcnow into now in action_status --- ti/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index a57d275..c262ac7 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python # coding: utf-8 """ @@ -237,7 +237,7 @@ def action_status(): current = data['work'][-1] start_time = parse_isotime(current['start']) - diff = timegap(start_time, datetime.utcnow()) + diff = timegap(start_time, datetime.now()) print('You have been working on {0} for {1}.'.format( green(current['name']), diff)) From 9f2385a6516f2abe3bcaabf2acf3fb1c96f8814f Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Tue, 23 Oct 2018 09:42:43 +0200 Subject: [PATCH 05/19] another utcnow to replace --- ti/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ti/__init__.py b/ti/__init__.py index c262ac7..d7882cc 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -268,7 +268,7 @@ def action_log(period): log[item['name']]['delta'] += ( parse_isotime(item['end']) - start_time) else: - log[item['name']]['delta'] += datetime.utcnow() - start_time + log[item['name']]['delta'] += datetime.now() - start_time current = item['name'] name_col_len = 0 From d5d00f1a55a67f775a7058d2135824522ad9bb4c Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Tue, 23 Oct 2018 10:08:33 +0200 Subject: [PATCH 06/19] sets seconds and miliseconds to 0 cuz I don't think it is needes and makes the script more complicated --- ti/__init__.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index d7882cc..8774e1b 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -44,6 +44,9 @@ from colorama import Fore +NOW = datetime.now().replace(second=0, microsecond=0) + + class TIError(Exception): """Errors raised by TI.""" @@ -237,7 +240,7 @@ def action_status(): current = data['work'][-1] start_time = parse_isotime(current['start']) - diff = timegap(start_time, datetime.now()) + diff = timegap(start_time, NOW) print('You have been working on {0} for {1}.'.format( green(current['name']), diff)) @@ -251,7 +254,8 @@ def action_log(period): comparedate = None if period: if period == "today": - comparedate = datetime.today() + comparedate = NOW + else: try: comparedate = datetime.strptime(period, "%Y-%m-%d") @@ -260,15 +264,15 @@ def action_log(period): for item in work: start_time = parse_isotime(item['start']) if (period and (start_time.year != comparedate.year or - start_time.month != comparedate.month or - start_time.day != comparedate.day)): + start_time.month != comparedate.month or + start_time.day != comparedate.day)): continue if 'end' in item: log[item['name']]['delta'] += ( parse_isotime(item['end']) - start_time) else: - log[item['name']]['delta'] += datetime.now() - start_time + log[item['name']]['delta'] += NOW - start_time current = item['name'] name_col_len = 0 @@ -347,28 +351,27 @@ def to_datetime(timestr): def parse_engtime(timestr): - now = datetime.now() if not timestr or timestr.strip() == 'now': - return now + return NOW match = re.match(r'(\d+|a) \s* (s|secs?|seconds?) \s+ ago $', timestr, re.X) if match is not None: n = match.group(1) seconds = 1 if n == 'a' else int(n) - return now - timedelta(seconds=seconds) + return NOW - timedelta(seconds=seconds) match = re.match(r'(\d+|a) \s* (mins?|minutes?) \s+ ago $', timestr, re.X) if match is not None: n = match.group(1) minutes = 1 if n == 'a' else int(n) - return now - timedelta(minutes=minutes) + return NOW - timedelta(minutes=minutes) match = re.match(r'(\d+|a|an) \s* (hrs?|hours?) \s+ ago $', timestr, re.X) if match is not None: n = match.group(1) hours = 1 if n in ['a', 'an'] else int(n) - return now - timedelta(hours=hours) + return NOW - timedelta(hours=hours) raise BadTime("Don't understand the time %r" % (timestr,)) @@ -403,6 +406,7 @@ def timegap(start_time, end_time): else: return 'more than a year' + def parse_args(argv=sys.argv): global use_color From a0f0b4e04524095bd9f1304dd5a76d33f2f9727e Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Tue, 23 Oct 2018 11:14:08 +0200 Subject: [PATCH 07/19] changing time format --- ti/__init__.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index 8774e1b..44b7157 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -346,7 +346,8 @@ def ensure_working(): def to_datetime(timestr): - return parse_engtime(timestr).isoformat() + 'Z' + return parse_engtime(timestr).strftime('%Y-%m-%d %H:%M') + def parse_engtime(timestr): @@ -354,13 +355,6 @@ def parse_engtime(timestr): if not timestr or timestr.strip() == 'now': return NOW - match = re.match(r'(\d+|a) \s* (s|secs?|seconds?) \s+ ago $', - timestr, re.X) - if match is not None: - n = match.group(1) - seconds = 1 if n == 'a' else int(n) - return NOW - timedelta(seconds=seconds) - match = re.match(r'(\d+|a) \s* (mins?|minutes?) \s+ ago $', timestr, re.X) if match is not None: n = match.group(1) @@ -377,7 +371,7 @@ def parse_engtime(timestr): def parse_isotime(isotime): - return datetime.strptime(isotime, '%Y-%m-%dT%H:%M:%S.%fZ') + return datetime.strptime(isotime, '%Y-%m-%d %H:%M') def timegap(start_time, end_time): From 0f6f45429c9f1864f3932d57a0d769d9d96e96f4 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Tue, 23 Oct 2018 14:40:22 +0200 Subject: [PATCH 08/19] simplify. no need for colors --- ti/__init__.py | 71 ++++++-------------------------------------------- 1 file changed, 8 insertions(+), 63 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index 44b7157..0736a55 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -41,7 +41,6 @@ from os import path import yaml -from colorama import Fore NOW = datetime.now().replace(second=0, microsecond=0) @@ -96,59 +95,13 @@ def dump(self, data): json.dump(data, f, separators=(',', ': '), indent=2) -def red(str): - if use_color: - return Fore.RED + str + Fore.RESET - else: - return str - - -def green(str): - if use_color: - return Fore.GREEN + str + Fore.RESET - else: - return str - - -def yellow(str): - if use_color: - return Fore.YELLOW + str + Fore.RESET - else: - return str - - -def blue(str): - if use_color: - return Fore.BLUE + str + Fore.RESET - else: - return str - - -color_regex = re.compile("(\x9B|\x1B\\[)[0-?]*[ -\/]*[@-~]") - - -def strip_color(str): - """Strip color from string.""" - return color_regex.sub("", str) - - -def len_color(str): - """Compute how long the color escape sequences in the string are.""" - return len(str) - len(strip_color(str)) - - -def ljust_with_color(str, n): - """ljust string that might contain color.""" - return str.ljust(n + len_color(str)) - - def action_on(name, time): data = store.load() work = data['work'] if work and 'end' not in work[-1]: raise AlreadyOn("You are already working on %s. Stop it or use a " - "different sheet." % (yellow(work[-1]['name']),)) + "different sheet." % (work[-1]['name'],)) entry = { 'name': name, @@ -158,7 +111,7 @@ def action_on(name, time): work.append(entry) store.dump(data) - print('Start working on ' + green(name) + '.') + print('Start working on ' + name + '.') def action_fin(time, back_from_interrupt=True): @@ -169,7 +122,7 @@ def action_fin(time, back_from_interrupt=True): current = data['work'][-1] current['end'] = time store.dump(data) - print('So you stopped working on ' + red(current['name']) + '.') + print('So you stopped working on ' + current['name'] + '.') if back_from_interrupt and len(data['interrupt_stack']) > 0: name = data['interrupt_stack'].pop()['name'] @@ -196,7 +149,7 @@ def action_interrupt(name, time): interrupt_stack.append(interrupted) store.dump(data) - action_on('interrupt: ' + green(name), time) + action_on('interrupt: ' + name, time) print('You are now %d deep in interrupts.' % len(interrupt_stack)) @@ -213,7 +166,7 @@ def action_note(content): store.dump(data) - print('Yep, noted to ' + yellow(current['name']) + '.') + print('Yep, noted to ' + current['name'] + '.') def action_tag(tags): @@ -243,7 +196,7 @@ def action_status(): diff = timegap(start_time, NOW) print('You have been working on {0} for {1}.'.format( - green(current['name']), diff)) + current['name'], diff)) def action_log(period): @@ -278,7 +231,7 @@ def action_log(period): name_col_len = 0 for name, item in log.items(): - name_col_len = max(name_col_len, len(strip_color(name))) + name_col_len = max(name_col_len, len(name)) secs = item['delta'].total_seconds() tmsg = [] @@ -299,7 +252,7 @@ def action_log(period): log[name]['tmsg'] = ', '.join(tmsg)[::-1].replace(',', '& ', 1)[::-1] for name, item in sorted(log.items(), key=(lambda x: x[0]), reverse=True): - print(ljust_with_color(name, name_col_len), ' ∙∙ ', item['tmsg'], + print(name.ljust(name_col_len), ' ∙∙ ', item['tmsg'], end=' ← working\n' if current == name else '\n') @@ -349,7 +302,6 @@ def to_datetime(timestr): return parse_engtime(timestr).strftime('%Y-%m-%d %H:%M') - def parse_engtime(timestr): if not timestr or timestr.strip() == 'now': @@ -402,12 +354,6 @@ def timegap(start_time, end_time): def parse_args(argv=sys.argv): - global use_color - - if '--no-color' in argv: - use_color = False - argv.remove('--no-color') - # prog = argv[0] if len(argv) == 1: raise BadArguments("You must specify a command.") @@ -486,7 +432,6 @@ def main(): store = JsonStore(os.getenv('SHEET_FILE', None) or os.path.expanduser('~/.ti-sheet')) -use_color = True if __name__ == '__main__': main() From f70da5bffbb000081269f0fb9183405ce01e70e5 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Tue, 23 Oct 2018 14:50:35 +0200 Subject: [PATCH 09/19] just simplifies it again. --- ti/__init__.py | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index 0736a55..857b7c2 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -193,10 +193,9 @@ def action_status(): current = data['work'][-1] start_time = parse_isotime(current['start']) - diff = timegap(start_time, NOW) - print('You have been working on {0} for {1}.'.format( - current['name'], diff)) + print('You have been working on {0} since {1}.'.format( + current['name'], start_time.strftime("%H:%M"))) def action_log(period): @@ -208,7 +207,6 @@ def action_log(period): if period: if period == "today": comparedate = NOW - else: try: comparedate = datetime.strptime(period, "%Y-%m-%d") @@ -246,9 +244,6 @@ def action_log(period): secs -= mins * 60 tmsg.append(str(mins) + ' minute' + ('s' if mins > 1 else '')) - if secs: - tmsg.append(str(secs) + ' second' + ('s' if secs > 1 else '')) - log[name]['tmsg'] = ', '.join(tmsg)[::-1].replace(',', '& ', 1)[::-1] for name, item in sorted(log.items(), key=(lambda x: x[0]), reverse=True): @@ -326,33 +321,6 @@ def parse_isotime(isotime): return datetime.strptime(isotime, '%Y-%m-%d %H:%M') -def timegap(start_time, end_time): - diff = end_time - start_time - - mins = diff.total_seconds() // 60 - - if mins == 0: - return 'less than a minute' - elif mins == 1: - return 'a minute' - elif mins < 44: - return '{} minutes'.format(mins) - elif mins < 89: - return 'about an hour' - elif mins < 1439: - return 'about {} hours'.format(mins // 60) - elif mins < 2519: - return 'about a day' - elif mins < 43199: - return 'about {} days'.format(mins // 1440) - elif mins < 86399: - return 'about a month' - elif mins < 525599: - return 'about {} months'.format(mins // 43200) - else: - return 'more than a year' - - def parse_args(argv=sys.argv): # prog = argv[0] if len(argv) == 1: From 8a1e49c3f3ad373fb02bf1b1d5eabd4dc6a73da9 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Tue, 23 Oct 2018 15:05:06 +0200 Subject: [PATCH 10/19] rename constant --- ti/__init__.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index 857b7c2..9a22a96 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -39,7 +39,6 @@ from datetime import datetime, timedelta from collections import defaultdict from os import path - import yaml @@ -96,7 +95,7 @@ def dump(self, data): def action_on(name, time): - data = store.load() + data = STORE.load() work = data['work'] if work and 'end' not in work[-1]: @@ -109,7 +108,7 @@ def action_on(name, time): } work.append(entry) - store.dump(data) + STORE.dump(data) print('Start working on ' + name + '.') @@ -117,16 +116,16 @@ def action_on(name, time): def action_fin(time, back_from_interrupt=True): ensure_working() - data = store.load() + data = STORE.load() current = data['work'][-1] current['end'] = time - store.dump(data) + STORE.dump(data) print('So you stopped working on ' + current['name'] + '.') if back_from_interrupt and len(data['interrupt_stack']) > 0: name = data['interrupt_stack'].pop()['name'] - store.dump(data) + STORE.dump(data) action_on(name, time) if len(data['interrupt_stack']) > 0: print('You are now %d deep in interrupts.' @@ -140,14 +139,14 @@ def action_interrupt(name, time): action_fin(time, back_from_interrupt=False) - data = store.load() + data = STORE.load() if 'interrupt_stack' not in data: data['interrupt_stack'] = [] interrupt_stack = data['interrupt_stack'] interrupted = data['work'][-1] interrupt_stack.append(interrupted) - store.dump(data) + STORE.dump(data) action_on('interrupt: ' + name, time) print('You are now %d deep in interrupts.' % len(interrupt_stack)) @@ -156,7 +155,7 @@ def action_interrupt(name, time): def action_note(content): ensure_working() - data = store.load() + data = STORE.load() current = data['work'][-1] if 'notes' not in current: @@ -164,7 +163,7 @@ def action_note(content): else: current['notes'].append(content) - store.dump(data) + STORE.dump(data) print('Yep, noted to ' + current['name'] + '.') @@ -172,14 +171,14 @@ def action_note(content): def action_tag(tags): ensure_working() - data = store.load() + data = STORE.load() current = data['work'][-1] current['tags'] = set(current.get('tags') or []) current['tags'].update(tags) current['tags'] = list(current['tags']) - store.dump(data) + STORE.dump(data) tag_count = len(tags) print("Okay, tagged current work with %d tag%s." @@ -189,7 +188,7 @@ def action_tag(tags): def action_status(): ensure_working() - data = store.load() + data = STORE.load() current = data['work'][-1] start_time = parse_isotime(current['start']) @@ -199,7 +198,7 @@ def action_status(): def action_log(period): - data = store.load() + data = STORE.load() work = data['work'] + data['interrupt_stack'] log = defaultdict(lambda: {'delta': timedelta()}) current = None @@ -255,7 +254,7 @@ def action_edit(): if "EDITOR" not in os.environ: raise NoEditor("Please set the 'EDITOR' environment variable") - data = store.load() + data = STORE.load() yml = yaml.safe_dump(data, default_flow_style=False, allow_unicode=True) cmd = os.getenv('EDITOR') @@ -276,11 +275,11 @@ def action_edit(): except: raise InvalidYAML("Oops, that YAML doesn't appear to be valid!") - store.dump(data) + STORE.dump(data) def is_working(): - data = store.load() + data = STORE.load() return data.get('work') and 'end' not in data['work'][-1] @@ -389,6 +388,7 @@ def parse_args(argv=sys.argv): def main(): + try: fn, args = parse_args() fn(**args) @@ -398,7 +398,7 @@ def main(): sys.exit(1) -store = JsonStore(os.getenv('SHEET_FILE', None) or +STORE = JsonStore(os.getenv('SHEET_FILE', None) or os.path.expanduser('~/.ti-sheet')) if __name__ == '__main__': From 609a4c24888b36c83fb09a72ea86166ffdab99e5 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Sat, 27 Oct 2018 07:03:01 +0200 Subject: [PATCH 11/19] using argparse for cli --- ti/__init__.py | 175 +++++++++++++++++++++++++++---------------------- 1 file changed, 97 insertions(+), 78 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index 9a22a96..2071aba 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -36,6 +36,7 @@ import subprocess import sys import tempfile +import argparse from datetime import datetime, timedelta from collections import defaultdict from os import path @@ -97,14 +98,13 @@ def dump(self, data): def action_on(name, time): data = STORE.load() work = data['work'] - if work and 'end' not in work[-1]: raise AlreadyOn("You are already working on %s. Stop it or use a " "different sheet." % (work[-1]['name'],)) entry = { 'name': name, - 'start': time, + 'start': NOW.strftime("%Y-%m-%d") + " " + time, } work.append(entry) @@ -119,7 +119,7 @@ def action_fin(time, back_from_interrupt=True): data = STORE.load() current = data['work'][-1] - current['end'] = time + current['end'] = NOW.strftime("%Y-%m-%d") + " " + time STORE.dump(data) print('So you stopped working on ' + current['name'] + '.') @@ -173,14 +173,15 @@ def action_tag(tags): data = STORE.load() current = data['work'][-1] - current['tags'] = set(current.get('tags') or []) - current['tags'].update(tags) + + for tag in tags.split(","): + current['tags'].add(tag) current['tags'] = list(current['tags']) STORE.dump(data) - tag_count = len(tags) + tag_count = len(tags.split(",")) print("Okay, tagged current work with %d tag%s." % (tag_count, "s" if tag_count > 1 else "")) @@ -197,33 +198,22 @@ def action_status(): current['name'], start_time.strftime("%H:%M"))) -def action_log(period): +def action_log(startdate, enddate): data = STORE.load() work = data['work'] + data['interrupt_stack'] log = defaultdict(lambda: {'delta': timedelta()}) current = None - comparedate = None - if period: - if period == "today": - comparedate = NOW - else: - try: - comparedate = datetime.strptime(period, "%Y-%m-%d") - except ValueError: - print("Please use dateformat yyyy-mm-dd") for item in work: start_time = parse_isotime(item['start']) - if (period and (start_time.year != comparedate.year or - start_time.month != comparedate.month or - start_time.day != comparedate.day)): - continue - - if 'end' in item: - log[item['name']]['delta'] += ( - parse_isotime(item['end']) - start_time) + if "end" not in item: + end_time = NOW + current = item["name"] else: - log[item['name']]['delta'] += NOW - start_time - current = item['name'] + end_time = parse_isotime(item['end']) + if (end_time.date() >= startdate.date() + and start_time.date() <= enddate.date()): + log[item['name']]['delta'] += ( + end_time - start_time) name_col_len = 0 @@ -320,69 +310,98 @@ def parse_isotime(isotime): return datetime.strptime(isotime, '%Y-%m-%d %H:%M') -def parse_args(argv=sys.argv): - # prog = argv[0] - if len(argv) == 1: - raise BadArguments("You must specify a command.") - - head = argv[1] - tail = argv[2:] - - if head in ['-h', '--help', 'h', 'help']: - raise BadArguments() - - elif head in ['e', 'edit']: - fn = action_edit - args = {} +def validate_time(s): + try: + return datetime.strptime(s, "%H:%M") + except ValueError: + msg = "Not a valid time hh:mm: '{0}'.".format(s) + raise argparse.ArgumentTypeError(msg) - elif head in ['o', 'on']: - if not tail: - raise BadArguments("Need the name of whatever you are working on.") +def validate_date(s): + try: + return datetime.strptime(s, "%Y-%m-%d") + except ValueError: + msg = "Not a valid date yyyy-mm-dd: '{0}'. U".format(s) + raise argparse.ArgumentTypeError(msg) + + +def parse_args(): + parser = argparse.ArgumentParser(description="ti is a simple and " + + "extensible time tracker for the" + + "command line.", prog="ti") + parser.add_argument("-o", "--on", action="store", + help="start an action to work on") + parser.add_argument("--at", action="store", help="start or stop actions at" + + "special time", + default=datetime.now().strftime("%H:%M"), + type=validate_time) + parser.add_argument("-l", "--log", action="store_true", + help="show log", default=False) + parser.add_argument("--start", action="store", + help="show log from", + default=datetime.now().strftime("%Y-%m-%d"), + type=validate_date) + parser.add_argument("--end", action="store", + help="show log from", + default=datetime.now().strftime("%Y-%m-%d"), + type=validate_date) + parser.add_argument("-f", "--fin", action="store_true", + help="end an action") + parser.add_argument("-t", "--tag", action="store", + help="at commasaperated tags to current task") + parser.add_argument("-s", "--status", action="store_true", + help="show status", default=False) + parser.add_argument("-e", "--edit", action="store_true", + help="show status", default=False) + parser.add_argument("-i", "--interrupt", action="store", + help="interrupt the current task with a new tas.") + parser.add_argument("--note", action="store", + help="add a note to the current task") + + arguments = parser.parse_args() + print(arguments) + if arguments.on: fn = action_on args = { - 'name': tail[0], - 'time': to_datetime(' '.join(tail[1:])), + 'name': arguments.on, + 'time': str(arguments.at.hour) + ":" + str(arguments.at.minute) + } + if arguments.interrupt: + fn = action_interrupt + args = { + 'name': arguments.interrupt, + 'time': str(arguments.at.hour) + ":" + str(arguments.at.minute) } - elif head in ['f', 'fin']: - fn = action_fin - args = {'time': to_datetime(' '.join(tail))} - - elif head in ['s', 'status']: - fn = action_status - args = {} - - elif head in ['l', 'log']: + elif arguments.log: fn = action_log - args = {'period': tail[0] if tail else None} - - elif head in ['t', 'tag']: - if not tail: - raise BadArguments("Please provide at least one tag to add.") - + args = { + 'startdate': arguments.start, + 'enddate': arguments.end + } + elif arguments.fin: + fn = action_fin + print(arguments.fin) + args = { + 'time': str(arguments.at.hour) + ":" + str(arguments.at.minute) + } + elif arguments.tag: fn = action_tag - args = {'tags': tail} - - elif head in ['n', 'note']: - if not tail: - raise BadArguments("Please provide some text to be noted.") - - fn = action_note - args = {'content': ' '.join(tail)} - - elif head in ['i', 'interrupt']: - if not tail: - raise BadArguments("Need the name of whatever you are working on.") - - fn = action_interrupt args = { - 'name': tail[0], - 'time': to_datetime(' '.join(tail[1:])), + 'tags': str(arguments.tag) } - + elif arguments.status: + fn = action_status + args = {} + elif arguments.edit: + fn = action_edit + args = {} + elif arguments.note: + fn = action_note + args = {'content': arguments.note} else: - raise BadArguments("I don't understand %r" % (head,)) + parser.print_help return fn, args From 020019b6af6196c5dfe7fd85029a4211f5189b6f Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Sat, 27 Oct 2018 13:28:58 +0200 Subject: [PATCH 12/19] change blank exception statement as it's not allowed --- ti/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ti/__init__.py b/ti/__init__.py index 2071aba..3ac41db 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -262,7 +262,8 @@ def action_edit(): try: data = yaml.load(yml) - except: + except (yaml.scanner.ScannerError, + yaml.parser.ParserError): raise InvalidYAML("Oops, that YAML doesn't appear to be valid!") STORE.dump(data) From 5a01cb1484382c1e845e47cdc6cc3eb56005e327 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Sat, 27 Oct 2018 13:31:50 +0200 Subject: [PATCH 13/19] remove unused exeption --- ti/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index 3ac41db..a8f28fa 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -70,10 +70,6 @@ class BadTime(TIError): """Time string can't be parsed.""" -class BadArguments(TIError): - """The command line arguments passed are not valid.""" - - class JsonStore(object): def __init__(self, filename): From 231ec1195f7e2088950aea587be4da82e1d771e0 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Mon, 29 Oct 2018 11:53:30 +0100 Subject: [PATCH 14/19] merge functionalioty pars_args() into main to reduce complexicity --- ti/__init__.py | 76 ++++++++++++++++---------------------------------- 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index a8f28fa..c49a064 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -323,7 +323,7 @@ def validate_date(s): raise argparse.ArgumentTypeError(msg) -def parse_args(): +def main(): parser = argparse.ArgumentParser(description="ti is a simple and " + "extensible time tracker for the" + "command line.", prog="ti") @@ -356,61 +356,33 @@ def parse_args(): parser.add_argument("--note", action="store", help="add a note to the current task") - arguments = parser.parse_args() - print(arguments) - if arguments.on: - fn = action_on - args = { - 'name': arguments.on, - 'time': str(arguments.at.hour) + ":" + str(arguments.at.minute) - } - if arguments.interrupt: - fn = action_interrupt - args = { - 'name': arguments.interrupt, - 'time': str(arguments.at.hour) + ":" + str(arguments.at.minute) - } - - elif arguments.log: - fn = action_log - args = { - 'startdate': arguments.start, - 'enddate': arguments.end - } - elif arguments.fin: - fn = action_fin - print(arguments.fin) - args = { - 'time': str(arguments.at.hour) + ":" + str(arguments.at.minute) - } - elif arguments.tag: - fn = action_tag - args = { - 'tags': str(arguments.tag) - } - elif arguments.status: - fn = action_status - args = {} - elif arguments.edit: - fn = action_edit - args = {} - elif arguments.note: - fn = action_note - args = {'content': arguments.note} - else: - parser.print_help - - return fn, args - - -def main(): - try: - fn, args = parse_args() - fn(**args) + arguments = parser.parse_args() + if arguments.on: + action_on(arguments.on, str(arguments.at.hour) + ":" + + str(arguments.at.minute)) + elif arguments.interrupt: + action_interrupt(arguments.interrupt, str(arguments.at.hour) + + ":" + str(arguments.at.minute)) + elif arguments.log: + action_log(arguments.start, arguments.end) + elif arguments.fin: + action_fin(str(arguments.at.hour) + ":" + str(arguments.at.minute)) + elif arguments.tag: + action_tag(str(arguments.tag)) + elif arguments.status: + action_status() + elif arguments.edit: + action_edit() + elif arguments.note: + action_note(arguments.note) + else: + parser.print_help() + except TIError as e: msg = str(e) if len(str(e)) > 0 else __doc__ print(msg, file=sys.stderr) + parser.print_help() sys.exit(1) From ae6bb55812b2efd2419d9098b3436a0ea422a086 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Mon, 29 Oct 2018 12:03:51 +0100 Subject: [PATCH 15/19] now Interrupt in job naming while interrupting --- ti/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ti/__init__.py b/ti/__init__.py index c49a064..fbddd60 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -144,7 +144,7 @@ def action_interrupt(name, time): interrupt_stack.append(interrupted) STORE.dump(data) - action_on('interrupt: ' + name, time) + action_on(name, time) print('You are now %d deep in interrupts.' % len(interrupt_stack)) From 6bc6daed412bade3b1813ef4296f4f3fda72e2e7 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Fri, 2 Nov 2018 12:43:02 +0100 Subject: [PATCH 16/19] adding tag output into log --- ti/__init__.py | 63 +++++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index fbddd60..bfe2a04 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -197,8 +197,8 @@ def action_status(): def action_log(startdate, enddate): data = STORE.load() work = data['work'] + data['interrupt_stack'] - log = defaultdict(lambda: {'delta': timedelta()}) current = None + tagList = defaultdict(lambda: {'delta': timedelta(), 'items': {}}) for item in work: start_time = parse_isotime(item['start']) if "end" not in item: @@ -206,34 +206,45 @@ def action_log(startdate, enddate): current = item["name"] else: end_time = parse_isotime(item['end']) + if "tags" not in item: + item["tags"] = ["_noTag_"] if (end_time.date() >= startdate.date() and start_time.date() <= enddate.date()): - log[item['name']]['delta'] += ( - end_time - start_time) - + delta = end_time - start_time + for tag in item["tags"]: + tagList[tag]['delta'] += delta + if item['name'] in tagList[tag]: + tagList[tag]['items'][item['name']] += delta + else: + tagList[tag]['items'][item['name']] = delta name_col_len = 0 - - for name, item in log.items(): - name_col_len = max(name_col_len, len(name)) - - secs = item['delta'].total_seconds() - tmsg = [] - - if secs > 3600: - hours = int(secs // 3600) - secs -= hours * 3600 - tmsg.append(str(hours) + ' hour' + ('s' if hours > 1 else '')) - - if secs > 60: - mins = int(secs // 60) - secs -= mins * 60 - tmsg.append(str(mins) + ' minute' + ('s' if mins > 1 else '')) - - log[name]['tmsg'] = ', '.join(tmsg)[::-1].replace(',', '& ', 1)[::-1] - - for name, item in sorted(log.items(), key=(lambda x: x[0]), reverse=True): - print(name.ljust(name_col_len), ' ∙∙ ', item['tmsg'], - end=' ← working\n' if current == name else '\n') + for tag, items in sorted(tagList.items(), key=(lambda x: x[0])): + print(tag.ljust(max(name_col_len, len(tag))), + "--", + time_to_string(items["delta"])) + for name, delta in sorted(items["items"].items(), + key=(lambda x: x[0]), + reverse=True): + print("\t", name.ljust(max(name_col_len, len(name))), + ' ∙∙ ', + time_to_string(delta), + end=' ← working\n' if current == name else '\n') + + +def time_to_string(time): + secs = time.total_seconds() + tmsg = [] + + if secs > 3600: + hours = int(secs // 3600) + secs -= hours * 3600 + tmsg.append(str(hours) + ' hour' + ('s' if hours > 1 else '')) + + if secs > 60: + mins = int(secs // 60) + secs -= mins * 60 + tmsg.append(str(mins) + ' minute' + ('s' if mins > 1 else '')) + return ', '.join(tmsg)[::-1].replace(',', '& ', 1)[::-1] def action_edit(): From fa4c073a08d51facb6322b2aba898065a017ead8 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Tue, 13 Nov 2018 11:02:44 +0100 Subject: [PATCH 17/19] repairs wrong if statement in action_log --- ti/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ti/__init__.py b/ti/__init__.py index bfe2a04..34f7311 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -213,7 +213,7 @@ def action_log(startdate, enddate): delta = end_time - start_time for tag in item["tags"]: tagList[tag]['delta'] += delta - if item['name'] in tagList[tag]: + if item['name'] in tagList[tag]['items'].keys(): tagList[tag]['items'][item['name']] += delta else: tagList[tag]['items'][item['name']] = delta From ffd3bfb71436b0e07a7c556caf421c68f5c742d0 Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Wed, 14 Nov 2018 10:35:49 +0100 Subject: [PATCH 18/19] when end interuption tags are now restored from the interrupted task --- ti/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index 34f7311..65ec939 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -120,9 +120,10 @@ def action_fin(time, back_from_interrupt=True): print('So you stopped working on ' + current['name'] + '.') if back_from_interrupt and len(data['interrupt_stack']) > 0: - name = data['interrupt_stack'].pop()['name'] + entry = data['interrupt_stack'].pop() STORE.dump(data) - action_on(name, time) + action_on(entry["name"], time) + action_tag(entry["tags"]) if len(data['interrupt_stack']) > 0: print('You are now %d deep in interrupts.' % len(data['interrupt_stack'])) @@ -171,13 +172,13 @@ def action_tag(tags): current = data['work'][-1] current['tags'] = set(current.get('tags') or []) - for tag in tags.split(","): + for tag in tags: current['tags'].add(tag) current['tags'] = list(current['tags']) STORE.dump(data) - tag_count = len(tags.split(",")) + tag_count = len(tags) print("Okay, tagged current work with %d tag%s." % (tag_count, "s" if tag_count > 1 else "")) @@ -380,7 +381,7 @@ def main(): elif arguments.fin: action_fin(str(arguments.at.hour) + ":" + str(arguments.at.minute)) elif arguments.tag: - action_tag(str(arguments.tag)) + action_tag(str(arguments.tag).split(",")) elif arguments.status: action_status() elif arguments.edit: From adc9f34a811738bb519d5dc84f357945cb48432d Mon Sep 17 00:00:00 2001 From: Ben Kenob1 Date: Tue, 27 Nov 2018 09:24:54 +0100 Subject: [PATCH 19/19] cleanup up the cli --- ti/__init__.py | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/ti/__init__.py b/ti/__init__.py index 65ec939..473d2ec 100755 --- a/ti/__init__.py +++ b/ti/__init__.py @@ -337,16 +337,26 @@ def validate_date(s): def main(): parser = argparse.ArgumentParser(description="ti is a simple and " - + "extensible time tracker for the" + + "extensible time tracker for the " + "command line.", prog="ti") - parser.add_argument("-o", "--on", action="store", - help="start an action to work on") + group = parser.add_mutually_exclusive_group() + group.add_argument("-o", "--on", action="store", + help="start an action to work on") + group.add_argument("-f", "--fin", action="store_true", + help="end an action") + group.add_argument("-s", "--status", action="store_true", + help="show status", default=False) + group.add_argument("-e", "--edit", action="store_true", + help="edit saved tasks", default=False) + group.add_argument("-i", "--interrupt", action="store", + help="interrupt the current task with a new task") + + group.add_argument("-l", "--log", action="store_true", + help="show log", default=False) parser.add_argument("--at", action="store", help="start or stop actions at" + "special time", default=datetime.now().strftime("%H:%M"), type=validate_time) - parser.add_argument("-l", "--log", action="store_true", - help="show log", default=False) parser.add_argument("--start", action="store", help="show log from", default=datetime.now().strftime("%Y-%m-%d"), @@ -355,16 +365,8 @@ def main(): help="show log from", default=datetime.now().strftime("%Y-%m-%d"), type=validate_date) - parser.add_argument("-f", "--fin", action="store_true", - help="end an action") parser.add_argument("-t", "--tag", action="store", - help="at commasaperated tags to current task") - parser.add_argument("-s", "--status", action="store_true", - help="show status", default=False) - parser.add_argument("-e", "--edit", action="store_true", - help="show status", default=False) - parser.add_argument("-i", "--interrupt", action="store", - help="interrupt the current task with a new tas.") + help="add commasaperated tags to current task") parser.add_argument("--note", action="store", help="add a note to the current task") @@ -373,23 +375,21 @@ def main(): if arguments.on: action_on(arguments.on, str(arguments.at.hour) + ":" + str(arguments.at.minute)) - elif arguments.interrupt: + if arguments.interrupt: action_interrupt(arguments.interrupt, str(arguments.at.hour) + ":" + str(arguments.at.minute)) - elif arguments.log: + if arguments.log: action_log(arguments.start, arguments.end) - elif arguments.fin: + if arguments.fin: action_fin(str(arguments.at.hour) + ":" + str(arguments.at.minute)) - elif arguments.tag: + if arguments.tag: action_tag(str(arguments.tag).split(",")) - elif arguments.status: + if arguments.status: action_status() - elif arguments.edit: + if arguments.edit: action_edit() - elif arguments.note: + if arguments.note: action_note(arguments.note) - else: - parser.print_help() except TIError as e: msg = str(e) if len(str(e)) > 0 else __doc__