From 5919ae223401501a734cc691588eae3e2409a2a3 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 14 Apr 2016 00:18:13 +0200 Subject: [PATCH 01/49] Refactor 'watson.frames': * Made 'watson.frames.Frames' a subclass of 'collections.OrderedDict'. * Changed order of items in 'watson.frames.Frame' to be more consistent with 'Frames.add()' and 'Frames.new_frame()'. This changes the on-disk JSON representation of the frames, i.e. the format of the '/watson/frames' file. The code handles loading the old format automatically and on saving the file gets written in the new format. * Removed ability to look up frame by index with dict look-up syntax and provide 'Frames.get_by_index()' method as replacement. * Removed ability to delete frame by index (was not used aywhere). * Removed ability to get Frames column values with dict look-up syntax and provide 'Frames.get_column()' generator as replacement. * Iterating over 'Watson.frames' yields frame IDs instead of 'Frame' instances. Where the latter is desired, the 'values()' method must be used. * Adding a frame by assigning a 'Frame' instance to a key of a `Frames`, makes a copy of the 'Frame' instance with the given key as the frame ID. * Made 'Frame.filter()' a generator. * Adapted code using 'Frame' and 'Frames' to these changes. * Adapted tests and test data files. + Command line interface remains unchanged. --- tests/resources/frames-with-conflict | 36 ++++----- tests/test_watson.py | 101 ++++++++++++++----------- watson/cli.py | 6 +- watson/frames.py | 109 +++++++++++---------------- watson/utils.py | 2 +- watson/watson.py | 10 ++- 6 files changed, 130 insertions(+), 134 deletions(-) diff --git a/tests/resources/frames-with-conflict b/tests/resources/frames-with-conflict index 53998ea1..0a01a81d 100644 --- a/tests/resources/frames-with-conflict +++ b/tests/resources/frames-with-conflict @@ -1,26 +1,26 @@ [ [ - 0, - 15, - "foo", - "1", - [], + "1", + "foo", + 0, + 15, + [], 15 - ], + ], [ - 20, - 50, - "bar", - "2", - [], + "2", + "bar", + 20, + 50, + [], 50 - ], + ], [ - 50, - 100, - "bar", - "3", - [], + "3", + "bar", + 50, + 100, + [], 100 ] -] \ No newline at end of file +] diff --git a/tests/test_watson.py b/tests/test_watson.py index 4868ee9d..5e69e9e5 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -147,25 +147,29 @@ def test_last_sync_with_empty_given_state(config_dir): # frames def test_frames(watson): - content = json.dumps([[0, 10, 'foo', None, ['A', 'B', 'C']]]) + content = json.dumps([['abcdefg', 'foo', 0, 10, ['A', 'B', 'C']]]) with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): assert len(watson.frames) == 1 - assert watson.frames[0].project == 'foo' - assert watson.frames[0].start == arrow.get(0) - assert watson.frames[0].stop == arrow.get(10) - assert watson.frames[0].tags == ['A', 'B', 'C'] + frame = watson.frames.get_by_index(0) + assert frame.id == 'abcdefg' + assert frame.project == 'foo' + assert frame.start == arrow.get(0) + assert frame.stop == arrow.get(10) + assert frame.tags == ['A', 'B', 'C'] def test_frames_without_tags(watson): - content = json.dumps([[0, 10, 'foo', None]]) + content = json.dumps([['abcdefg', 'foo', 0, 10]]) with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): assert len(watson.frames) == 1 - assert watson.frames[0].project == 'foo' - assert watson.frames[0].start == arrow.get(0) - assert watson.frames[0].stop == arrow.get(10) - assert watson.frames[0].tags == [] + frame = watson.frames.get_by_index(0) + assert frame.id == 'abcdefg' + assert frame.project == 'foo' + assert frame.start == arrow.get(0) + assert frame.stop == arrow.get(10) + assert frame.tags == [] def test_frames_with_empty_file(watson): @@ -190,18 +194,19 @@ def test_frames_watson_non_valid_json(watson): def test_given_frames(config_dir): - content = json.dumps([[0, 10, 'foo', None, ['A']]]) - watson = Watson(frames=[[0, 10, 'bar', None, ['A', 'B']]], + content = json.dumps([['abcdefg', 'foo', 0, 10, ['A']]]) + watson = Watson(frames=[['abcdefg', 'bar', 0, 10, ['A', 'B']]], config_dir=config_dir) with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): assert len(watson.frames) == 1 - assert watson.frames[0].project == 'bar' - assert watson.frames[0].tags == ['A', 'B'] + frame = watson.frames.get_by_index(0) + assert frame.project == 'bar' + assert frame.tags == ['A', 'B'] def test_frames_with_empty_given_state(config_dir): - content = json.dumps([[0, 10, 'foo', None, ['A']]]) + content = json.dumps([['abcdefg', 'foo', 0, 10, ['A']]]) watson = Watson(frames=[], config_dir=config_dir) with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): @@ -370,10 +375,11 @@ def test_stop_started_project(watson): assert watson.current == {} assert watson.is_started is False assert len(watson.frames) == 1 - assert watson.frames[0].project == 'foo' - assert isinstance(watson.frames[0].start, arrow.Arrow) - assert isinstance(watson.frames[0].stop, arrow.Arrow) - assert watson.frames[0].tags == ['A', 'B'] + frame = watson.frames.get_by_index(0) + assert frame.project == 'foo' + assert isinstance(frame.start, arrow.Arrow) + assert isinstance(frame.stop, arrow.Arrow) + assert frame.tags == ['A', 'B'] def test_stop_started_project_without_tags(watson): @@ -383,10 +389,11 @@ def test_stop_started_project_without_tags(watson): assert watson.current == {} assert watson.is_started is False assert len(watson.frames) == 1 - assert watson.frames[0].project == 'foo' - assert isinstance(watson.frames[0].start, arrow.Arrow) - assert isinstance(watson.frames[0].stop, arrow.Arrow) - assert watson.frames[0].tags == [] + frame = watson.frames.get_by_index(0) + assert frame.project == 'foo' + assert isinstance(frame.start, arrow.Arrow) + assert isinstance(frame.stop, arrow.Arrow) + assert frame.tags == [] def test_stop_no_project(watson): @@ -465,7 +472,7 @@ def test_save_empty_current(config_dir): def test_save_frames_no_change(config_dir): - watson = Watson(frames=[[0, 10, 'foo', None]], + watson = Watson(frames=[['abcdefg', 'foo', 0, 10]], config_dir=config_dir) with mock.patch('%s.open' % builtins, mock.mock_open()): @@ -476,8 +483,8 @@ def test_save_frames_no_change(config_dir): def test_save_added_frame(config_dir): - watson = Watson(frames=[[0, 10, 'foo', None]], config_dir=config_dir) - watson.frames.add('bar', 10, 20, ['A']) + watson = Watson(frames=[['abcdefg', 'foo', 0, 10]], config_dir=config_dir) + watson.frames.add('bar', 10, 20, tags=['A']) with mock.patch('%s.open' % builtins, mock.mock_open()): with mock.patch('json.dump') as json_mock: @@ -486,16 +493,21 @@ def test_save_added_frame(config_dir): assert json_mock.call_count == 1 result = json_mock.call_args[0][0] assert len(result) == 2 - assert result[0][2] == 'foo' + assert result[0][0] == 'abcdefg' + assert result[0][1] == 'foo' + assert result[0][2] == 0 + assert result[0][3] == 10 assert result[0][4] == [] - assert result[1][2] == 'bar' + assert result[1][1] == 'bar' + assert result[1][2] == 10 + assert result[1][3] == 20 assert result[1][4] == ['A'] def test_save_changed_frame(config_dir): - watson = Watson(frames=[[0, 10, 'foo', None, ['A']]], + watson = Watson(frames=[['abcdefg', 'foo', 0, 10, ['A']]], config_dir=config_dir) - watson.frames[0] = ('bar', 0, 10, ['A', 'B']) + watson.frames['abcdefg'] = ('bar', 0, 10, ['A', 'B']) with mock.patch('%s.open' % builtins, mock.mock_open()): with mock.patch('json.dump') as json_mock: @@ -504,7 +516,10 @@ def test_save_changed_frame(config_dir): assert json_mock.call_count == 1 result = json_mock.call_args[0][0] assert len(result) == 1 - assert result[0][2] == 'bar' + assert result[0][0] == 'abcdefg' + assert result[0][1] == 'bar' + assert result[0][2] == 0 + assert result[0][3] == 10 assert result[0][4] == ['A', 'B'] dump_args = json_mock.call_args[1] @@ -716,17 +731,19 @@ def json(self): assert len(watson.frames) == 2 - assert watson.frames[0].id == '1' - assert watson.frames[0].project == 'foo' - assert watson.frames[0].start.timestamp == 3 - assert watson.frames[0].stop.timestamp == 4 - assert watson.frames[0].tags == ['A'] - - assert watson.frames[1].id == '2' - assert watson.frames[1].project == 'bar' - assert watson.frames[1].start.timestamp == 4 - assert watson.frames[1].stop.timestamp == 5 - assert watson.frames[1].tags == [] + frame = watson.frames.get_by_index(0) + assert frame.id == '1' + assert frame.project == 'foo' + assert frame.start.timestamp == 3 + assert frame.stop.timestamp == 4 + assert frame.tags == ['A'] + + frame = watson.frames.get_by_index(1) + assert frame.id == '2' + assert frame.project == 'bar' + assert frame.start.timestamp == 4 + assert frame.stop.timestamp == 5 + assert frame.tags == [] # projects diff --git a/watson/cli.py b/watson/cli.py index e1630401..11ee8fd0 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -568,7 +568,7 @@ def frames(watson): [...] """ for frame in watson.frames: - click.echo(style('short_id', frame.id)) + click.echo(style('short_id', frame)) @cli.command(context_settings={'ignore_unknown_options': True}) @@ -596,10 +596,10 @@ def edit(watson, id): frame = get_frame_from_argument(watson, id) id = frame.id elif watson.is_started: - frame = Frame(watson.current['start'], None, watson.current['project'], + frame = Frame(None, watson.current['project'], watson.current['start'], None, watson.current['tags']) elif watson.frames: - frame = watson.frames[-1] + frame = watson.frames.get_by_index(-1) id = frame.id else: raise click.ClickException( diff --git a/watson/frames.py b/watson/frames.py index f9d5137a..dec7d4ae 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -1,14 +1,16 @@ +# -*- coding: utf-8 -*- + import uuid import arrow -from collections import namedtuple +from collections import OrderedDict, namedtuple -HEADERS = ('start', 'stop', 'project', 'id', 'tags', 'updated_at') +FIELDS = ('id', 'project', 'start', 'stop', 'tags', 'updated_at') -class Frame(namedtuple('Frame', HEADERS)): - def __new__(cls, start, stop, project, id, tags=None, updated_at=None,): +class Frame(namedtuple('Frame', FIELDS)): + def __new__(cls, id, project, start, stop, tags=None, updated_at=None): try: if not isinstance(start, arrow.Arrow): start = arrow.get(start) @@ -31,7 +33,7 @@ def __new__(cls, start, stop, project, id, tags=None, updated_at=None,): tags = [] return super(Frame, cls).__new__( - cls, start, stop, project, id, tags, updated_at + cls, id, project, start, stop, tags, updated_at ) def dump(self): @@ -39,7 +41,7 @@ def dump(self): stop = self.stop.to('utc').timestamp updated_at = self.updated_at.timestamp - return (start, stop, self.project, self.id, self.tags, updated_at) + return (self.id, self.project, start, stop, self.tags, updated_at) @property def day(self): @@ -68,88 +70,63 @@ def __contains__(self, frame): return frame.start >= self.start and frame.stop <= self.stop -class Frames(object): +class Frames(OrderedDict): def __init__(self, frames=None): - if not frames: - frames = [] + super(Frames, self).__init__() + for frame in frames or []: + # convert from old format with project @ idx 2 and ID @ idx 3 + if not isinstance(frame[2], (int, float)): + frame = [frame[3], frame[2], frame[0], frame[1]] + frame[4:] - rows = [Frame(*frame) for frame in frames] - self._rows = rows + frame = Frame(*frame) + self[frame.id] = frame self.changed = False - def __len__(self): - return len(self._rows) - - def __getitem__(self, key): - if key in HEADERS: - return tuple(self._get_col(key)) - elif isinstance(key, int): - return self._rows[key] - else: - return self._rows[self._get_index_by_id(key)] - def __setitem__(self, key, value): - self.changed = True - if isinstance(value, Frame): - frame = value + frame = self.new_frame(value.project, value.start, value.stop, + value.tags, value.updated_at, id=key) else: - frame = self.new_frame(*value) + frame = self.new_frame(*value[:5], id=key) - if isinstance(key, int): - self._rows[key] = frame - else: - frame = frame._replace(id=key) - try: - self._rows[self._get_index_by_id(key)] = frame - except KeyError: - self._rows.append(frame) + super(Frames, self).__setitem__(key, frame) + self.changed = True def __delitem__(self, key): + super(Frames, self).__delitem__(key) self.changed = True - if isinstance(key, int): - del self._rows[key] - else: - del self._rows[self._get_index_by_id(key)] - - def _get_index_by_id(self, id): - try: - return next( - i for i, v in enumerate(self['id']) if v.startswith(id) - ) - except StopIteration: - raise KeyError("Frame with id {} not found.".format(id)) - - def _get_col(self, col): - index = HEADERS.index(col) - for row in self._rows: - yield row[index] - def add(self, *args, **kwargs): - self.changed = True frame = self.new_frame(*args, **kwargs) - self._rows.append(frame) + self[frame.id] = frame return frame - def new_frame(self, project, start, stop, tags=None, id=None, - updated_at=None): - if not id: + def new_frame(self, project, start, stop, tags=None, updated_at=None, + id=None): + if id is None: id = uuid.uuid4().hex - return Frame(start, stop, project, id, tags=tags, - updated_at=updated_at) + + return Frame(id, project, start, stop, tags, updated_at) def dump(self): - return tuple(frame.dump() for frame in self._rows) + return tuple(frame.dump() for frame in self.values()) def filter(self, projects=None, tags=None, span=None): - return ( - frame for frame in self._rows - if (projects is None or frame.project in projects) and - (tags is None or any(tag in frame.tags for tag in tags)) and - (span is None or frame in span) - ) + for frame in self.values(): + if ((projects is None or frame.project in projects) and + (tags is None or any(tag in frame.tags for tag in tags)) and + (span is None or frame in span)): + yield frame + + def get_by_index(self, index): + key = list(self.keys())[index] + return self[key] + + def get_column(self, col): + index = FIELDS.index(col) + for row in self.values(): + yield row[index] def span(self, start, stop): return Span(start, stop) diff --git a/watson/utils.py b/watson/utils.py index 3f17580d..da04214b 100644 --- a/watson/utils.py +++ b/watson/utils.py @@ -95,7 +95,7 @@ def get_frame_from_argument(watson, arg): try: index = int(arg) if index < 0: - return watson.frames[index] + return watson.frames.get_by_index(index) except IndexError: raise click.ClickException( style('error', "No frame found for index {}.".format(arg)) diff --git a/watson/watson.py b/watson/watson.py index db16c981..78513c07 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import itertools import json try: @@ -275,14 +276,15 @@ def projects(self): """ Return the list of all the existing projects, sorted by name. """ - return sorted(set(self.frames['project'])) + return sorted(set(self.frames.get_column('project'))) @property def tags(self): """ Return the list of the tags, sorted by name. """ - return sorted(set(tag for tags in self.frames['tags'] for tag in tags)) + return sorted( + set(itertools.chain.from_iterable(self.frames.get_column('tags')))) def _get_request_info(self, route): config = self.config @@ -368,7 +370,7 @@ def push(self, last_pull): frames = [] - for frame in self.frames: + for frame in self.frames.values(): if last_pull > frame.updated_at > self.last_sync: try: # Find the url of the project @@ -411,7 +413,7 @@ def merge_report(self, frames_with_conflict): conflicting = [] merging = [] - for conflict_frame in conflict_file_frames: + for conflict_frame in conflict_file_frames.values(): try: original_frame = self.frames[conflict_frame.id] From 92b8733b66c41ef4d8bb5687994d979b28c1d6d3 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 14 Apr 2016 01:06:56 +0200 Subject: [PATCH 02/49] Fix flake8 complaint by refactoring boolean expr --- watson/frames.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watson/frames.py b/watson/frames.py index dec7d4ae..bd502e70 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -115,8 +115,8 @@ def dump(self): def filter(self, projects=None, tags=None, span=None): for frame in self.values(): if ((projects is None or frame.project in projects) and - (tags is None or any(tag in frame.tags for tag in tags)) and - (span is None or frame in span)): + (tags is None or set(frame.tags).intersection(tags)) and + (span is None or frame in span)): yield frame def get_by_index(self, index): From a1a905a336fd05d4c99d613509e001c602ea73a3 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 14 Apr 2016 18:23:26 +0200 Subject: [PATCH 03/49] Optimize index access to Frames and add tests for 'get_by_index' --- tests/test_watson.py | 60 +++++++++++++++++++++++++++++++++++++++++--- watson/frames.py | 14 +++++++++-- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index 5e69e9e5..17578081 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -151,7 +151,7 @@ def test_frames(watson): with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): assert len(watson.frames) == 1 - frame = watson.frames.get_by_index(0) + frame = watson.frames['abcdefg'] assert frame.id == 'abcdefg' assert frame.project == 'foo' assert frame.start == arrow.get(0) @@ -159,12 +159,66 @@ def test_frames(watson): assert frame.tags == ['A', 'B', 'C'] +def test_frames_get_by_index(watson): + test_frames = ( + ('0', ('project0', 0, 10)), + ('1', ('project1', 10, 20)), + ('2', ('project2', 20, 30)), + ('3', ('project3', 30, 40)), + ('4', ('project4', 40, 50)) + ) + + for id, frame in test_frames[:-1]: + watson.frames[id] = frame + + assert len(watson.frames) == 4 + assert watson.frames.get_by_index(0).id == '0' + assert watson.frames.get_by_index(0).project == 'project0' + assert watson.frames.get_by_index(2).id == '2' + assert watson.frames.get_by_index(2).project == 'project2' + assert watson.frames.get_by_index(-1).id == '3' + assert watson.frames.get_by_index(-1).project == 'project3' + + # adding an item + id, frame = test_frames[-1] + watson.frames[id] = frame + assert len(watson.frames) == 5 + assert watson.frames.get_by_index(3).id == '3' + assert watson.frames.get_by_index(-1).id == '4' + + # setting an existing item + assert watson.frames.get_by_index(2).project == 'project2' + watson.frames['2'] = ('project6', 50, 60) + assert len(watson.frames) == 5 + assert watson.frames.get_by_index(2).project == 'project6' + + # deleting an item + del watson.frames['2'] + assert len(watson.frames) == 4 + assert watson.frames.get_by_index(2).id == '3' + assert watson.frames.get_by_index(2).project == 'project3' + + # index out of range + with pytest.raises(IndexError): + watson.frames.get_by_index(4) + + if not PY2: + # move_to_end + assert watson.frames.get_by_index(0).project == 'project0' + watson.frames.move_to_end('0') + assert watson.frames.get_by_index(-1).project == 'project0' + + assert watson.frames.get_by_index(0).project == 'project1' + watson.frames.move_to_end('4', False) + assert watson.frames.get_by_index(0).project == 'project4' + + def test_frames_without_tags(watson): content = json.dumps([['abcdefg', 'foo', 0, 10]]) with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): assert len(watson.frames) == 1 - frame = watson.frames.get_by_index(0) + frame = watson.frames['abcdefg'] assert frame.id == 'abcdefg' assert frame.project == 'foo' assert frame.start == arrow.get(0) @@ -200,7 +254,7 @@ def test_given_frames(config_dir): with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): assert len(watson.frames) == 1 - frame = watson.frames.get_by_index(0) + frame = watson.frames['abcdefg'] assert frame.project == 'bar' assert frame.tags == ['A', 'B'] diff --git a/watson/frames.py b/watson/frames.py index bd502e70..1ed58679 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -73,6 +73,8 @@ def __contains__(self, frame): class Frames(OrderedDict): def __init__(self, frames=None): super(Frames, self).__init__() + self._keys = list(self.keys()) + for frame in frames or []: # convert from old format with project @ idx 2 and ID @ idx 3 if not isinstance(frame[2], (int, float)): @@ -90,13 +92,22 @@ def __setitem__(self, key, value): else: frame = self.new_frame(*value[:5], id=key) + if key not in self: + self._keys.append(key) + super(Frames, self).__setitem__(key, frame) self.changed = True def __delitem__(self, key): super(Frames, self).__delitem__(key) + self._keys.remove(key) self.changed = True + def move_to_end(self, key, last=True): + super(Frames, self).move_to_end(key, last) + self._keys.remove(key) + self._keys.insert(len(self._keys) if last else 0, key) + def add(self, *args, **kwargs): frame = self.new_frame(*args, **kwargs) self[frame.id] = frame @@ -120,8 +131,7 @@ def filter(self, projects=None, tags=None, span=None): yield frame def get_by_index(self, index): - key = list(self.keys())[index] - return self[key] + return self[self._keys[index]] def get_column(self, col): index = FIELDS.index(col) From 2ab1991a81962b12d8b79275e57746810a3c5727 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 26 May 2016 20:15:10 +0200 Subject: [PATCH 04/49] Update merge command to work with new frame format --- watson/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index 11ee8fd0..203708be 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -932,9 +932,9 @@ def merge(watson, frames_with_conflict, force): # merge in any non-conflicting frames for frame in merging: - start, stop, project, id, tags, updated_at = frame.dump() - original_frames.add(project, start, stop, tags=tags, id=id, - updated_at=updated_at) + id, project, start, stop, tags, updated_at = frame.dump() + original_frames.add(project, start, stop, tags=tags, + updated_at=updated_at, id=id) watson.frames = original_frames watson.frames.changed = True From 8ed29c8f82ce7734796d1997736ad75eedf5a5f1 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 26 May 2016 21:35:03 +0200 Subject: [PATCH 05/49] Remove uneccessary assignment since both names already refer to the same object Signed-off-by: Christopher Arndt --- watson/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/watson/cli.py b/watson/cli.py index 203708be..94856dc3 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -936,6 +936,5 @@ def merge(watson, frames_with_conflict, force): original_frames.add(project, start, stop, tags=tags, updated_at=updated_at, id=id) - watson.frames = original_frames watson.frames.changed = True watson.save() From 553e34a73799d5e94941a84da94337d244ddaf53 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 26 May 2016 21:56:49 +0200 Subject: [PATCH 06/49] Assigning a Frames instance to watson.frames should work with new Frames format too; Clarify docstring on passing frames to construcutor as well Signed-off-by: Christopher Arndt --- watson/watson.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/watson/watson.py b/watson/watson.py index 78513c07..c7a64407 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -29,10 +29,17 @@ class ConfigurationError(WatsonError, configparser.Error): class Watson(object): def __init__(self, **kwargs): """ - :param frames: If given, should be a list representating the - frames. - If not given, the value is extracted - from the frames file. + :param frames: If given, should be a sequence of frames or a + frames.Frames instance. + + If a sequence is given, each item may either be a + frames.Frame instance or a sequence of frame values, + with at least these 4 items: + + (id, project, start, stop) + + If not given, the value is extracted from the frames + file. :type frames: list :param current: If given, should be a dict representating the @@ -177,7 +184,10 @@ def frames(self): @frames.setter def frames(self, frames): - self._frames = Frames(frames) + if isinstance(frames, Frames): + self._frames = frames + else: + self._frames = Frames(frames) @property def current(self): From 714b58159b89f56cf301d5ac429c7c6b32a16ba7 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 26 May 2016 21:57:39 +0200 Subject: [PATCH 07/49] Make sure passing a sequence of frames works with tuple and list items Signed-off-by: Christopher Arndt --- watson/frames.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/watson/frames.py b/watson/frames.py index 1ed58679..ba661588 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -78,7 +78,12 @@ def __init__(self, frames=None): for frame in frames or []: # convert from old format with project @ idx 2 and ID @ idx 3 if not isinstance(frame[2], (int, float)): - frame = [frame[3], frame[2], frame[0], frame[1]] + frame[4:] + frame = ( + frame[3], # id + frame[2], # project + frame[0], # start + frame[1] # stop + ) + tuple(frame[4:]) frame = Frame(*frame) self[frame.id] = frame From 73c25931b40b6ebca294219eb46354153e585485 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 26 May 2016 21:58:23 +0200 Subject: [PATCH 08/49] Add more tests for setting watson.frames --- tests/test_watson.py | 101 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/tests/test_watson.py b/tests/test_watson.py index 17578081..6addf6e2 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -19,6 +19,7 @@ from click import get_app_dir from watson import Watson, WatsonError +from watson.frames import Frame, Frames from watson.watson import ConfigurationError, ConfigParser TEST_FIXTURE_DIR = py.path.local( @@ -159,6 +160,106 @@ def test_frames(watson): assert frame.tags == ['A', 'B', 'C'] +def test_frames_from_sequence_of_tuples(watson): + test_frames = ( + ('0', 'project0', 0, 10), + ('1', 'project1', 10, 20, ['A', 'B', 'C']) + ) + watson.frames = test_frames + + assert len(watson.frames) == 2 + assert isinstance(watson.frames, Frames) + + frame = watson.frames.get_by_index(0) + assert isinstance(frame, Frame) + assert frame.id == '0' + assert frame.project == 'project0' + assert frame.start == arrow.get(0) + assert frame.stop == arrow.get(10) + + frame = watson.frames.get_by_index(1) + assert isinstance(frame, Frame) + assert frame.id == '1' + assert frame.project == 'project1' + assert frame.start == arrow.get(10) + assert frame.stop == arrow.get(20) + assert frame.tags == ['A', 'B', 'C'] + + +def test_frames_from_sequence_of_lists(watson): + test_frames = ( + ['0', 'project0', 0, 10], + ['1', 'project1', 10, 20, ['A', 'B', 'C']] + ) + watson.frames = test_frames + + assert len(watson.frames) == 2 + assert isinstance(watson.frames, Frames) + + frame = watson.frames.get_by_index(0) + assert isinstance(frame, Frame) + assert frame.id == '0' + assert frame.project == 'project0' + assert frame.start == arrow.get(0) + assert frame.stop == arrow.get(10) + + frame = watson.frames.get_by_index(1) + assert isinstance(frame, Frame) + assert frame.id == '1' + assert frame.project == 'project1' + assert frame.start == arrow.get(10) + assert frame.stop == arrow.get(20) + assert frame.tags == ['A', 'B', 'C'] + + +def test_frames_from_old_format(watson): + test_frames = ( + [0, 10, 'project0', '0'], + [10, 20, 'project1', '1', ['A', 'B', 'C']], + [20, 30, 'project2', '2', ['D', 'E', 'F'], 30] + ) + watson.frames = test_frames + + assert len(watson.frames) == 3 + assert isinstance(watson.frames, Frames) + + frame = watson.frames.get_by_index(0) + assert isinstance(frame, Frame) + assert frame.id == '0' + assert frame.project == 'project0' + assert frame.start == arrow.get(0) + assert frame.stop == arrow.get(10) + + frame = watson.frames.get_by_index(1) + assert isinstance(frame, Frame) + assert frame.id == '1' + assert frame.project == 'project1' + assert frame.start == arrow.get(10) + assert frame.stop == arrow.get(20) + assert frame.tags == ['A', 'B', 'C'] + + frame = watson.frames.get_by_index(2) + assert isinstance(frame, Frame) + assert frame.id == '2' + assert frame.project == 'project2' + assert frame.start == arrow.get(20) + assert frame.stop == arrow.get(30) + assert frame.tags == ['D', 'E', 'F'] + assert frame.updated_at == arrow.get(30) + + +def test_frames_from_frames(watson): + frames_instance = Frames(( + ('0', 'project0', 0, 10), + ('1', 'project1', 10, 20, ['A', 'B', 'C']) + )) + watson.frames = frames_instance + + assert len(watson.frames) == 2 + assert isinstance(watson.frames, Frames) + assert watson.frames is frames_instance + + def test_frames_get_by_index(watson): test_frames = ( ('0', ('project0', 0, 10)), From c956089e176a0c6792be4ebe927e5dbc605e85f7 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 9 Jun 2016 22:55:01 +0200 Subject: [PATCH 09/49] (Re-)add support for looking up frames by ID prefix Signed-off-by: Christopher Arndt --- tests/test_watson.py | 21 +++++++++++++++++++++ watson/frames.py | 10 ++++++++++ 2 files changed, 31 insertions(+) diff --git a/tests/test_watson.py b/tests/test_watson.py index 6addf6e2..e56a75e2 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -314,6 +314,27 @@ def test_frames_get_by_index(watson): assert watson.frames.get_by_index(0).project == 'project4' +def test_frames_get_by_id(watson): + test_frames = ( + ('abcdef', ('project0', 0, 10)), + ('abcxyz', ('project1', 10, 20)), + ('defghi', ('project2', 20, 30)), + ) + + for id, frame in test_frames: + watson.frames[id] = frame + + frame = watson.frames['abcdef'] + assert frame.project == 'project0' + frame = watson.frames['abcxyz'] + assert frame.project == 'project1' + + frame = watson.frames['abc'] + assert frame.project == 'project0' + frame = watson.frames['def'] + assert frame.project == 'project2' + + def test_frames_without_tags(watson): content = json.dumps([['abcdefg', 'foo', 0, 10]]) diff --git a/watson/frames.py b/watson/frames.py index ba661588..c61f5bbc 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -90,6 +90,16 @@ def __init__(self, frames=None): self.changed = False + def __getitem__(self, id): + try: + return super(Frames, self).__getitem__(id) + except KeyError: + for key in self._keys: + if key.startswith(id): + return super(Frames, self).__getitem__(key) + else: + raise KeyError("Frame with id {} not found.".format(id)) + def __setitem__(self, key, value): if isinstance(value, Frame): frame = self.new_frame(value.project, value.start, value.stop, From 8d2206ff3f1eea4512df3896ff57e01549158998 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Sat, 11 Jun 2016 15:32:58 +0200 Subject: [PATCH 10/49] When looking up a frame by ID prefix, iterate over keys in reverse order for better perfomance in most likely case Signed-off-by: Christopher Arndt --- tests/test_watson.py | 2 +- watson/frames.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index e56a75e2..2b783104 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -330,7 +330,7 @@ def test_frames_get_by_id(watson): assert frame.project == 'project1' frame = watson.frames['abc'] - assert frame.project == 'project0' + assert frame.project == 'project1' frame = watson.frames['def'] assert frame.project == 'project2' diff --git a/watson/frames.py b/watson/frames.py index c61f5bbc..77a085bb 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -94,7 +94,7 @@ def __getitem__(self, id): try: return super(Frames, self).__getitem__(id) except KeyError: - for key in self._keys: + for key in reversed(self._keys): if key.startswith(id): return super(Frames, self).__getitem__(key) else: From c7c3c84f8756a9861f7ec520db8906f273e83a25 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Sat, 11 Jun 2016 22:53:22 +0200 Subject: [PATCH 11/49] Add script to convert between old and new frame format (for development) Signed-off-by: Christopher Arndt --- scripts/conv_frames.py | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100755 scripts/conv_frames.py diff --git a/scripts/conv_frames.py b/scripts/conv_frames.py new file mode 100755 index 00000000..443c3621 --- /dev/null +++ b/scripts/conv_frames.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Convert the watson frames file from the old to new format and vice versa.""" + +from __future__ import print_function + +import json +import os +import tempfile + +import click + + +app_dir = click.get_app_dir('watson') +frames_file = os.path.join(app_dir, 'frames') +backup_file = frames_file + '.old' + +with open(frames_file) as fp: + frames = json.load(fp) + +if frames: + if isinstance(frames[0][0], int) and isinstance(frames[0][1], int): + print("Converting frames to new format...") + converted_frames = [[f[3], f[2], f[0], f[1], f[4], f[5]] + for f in frames] + else: + print("Converting frames to old format...") + converted_frames = [[f[2], f[3], f[1], f[0], f[4], f[5]] + for f in frames] + + try: + with tempfile.NamedTemporaryFile('w', delete=False) as tmpfp: + json.dump(converted_frames, tmpfp, indent=1, ensure_ascii=False) + + if os.path.exists(backup_file): + raise IOError("Backup file '{}' already exists. " + "Will not overwrite.".format(backup_file)) + else: + os.rename(frames_file, backup_file) + + os.rename(tmpfp.name, frames_file) + finally: + try: + os.unlink(tmpfp.name) + except: + pass +else: + print("No frames found. Nothing to do.") From 909e091bf6c3bb9081c26040ddfdd1b9cdcc5c28 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 26 May 2016 19:27:28 +0200 Subject: [PATCH 12/49] Add log message to frames and allow setting it with stop or edit command --- watson/cli.py | 40 +++++++++++++++++++++++++++++++++------- watson/frames.py | 19 +++++++++++-------- watson/utils.py | 3 ++- watson/watson.py | 9 +++++---- 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index 94856dc3..3c73a3d8 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -134,8 +134,10 @@ def start(ctx, watson, args): @cli.command() +@click.option('-m', '--message', 'message', default=None, + help="Save given log message with the project frame.") @click.pass_obj -def stop(watson): +def stop(watson, message): """ Stop monitoring time for the current project. @@ -145,6 +147,9 @@ def stop(watson): $ watson stop Stopping project apollo11, started a minute ago. (id: e7ccd52) """ + if watson.is_started and message is not None: + watson._current['message'] = message + frame = watson.stop() click.echo("Stopping project {} {}, started {}. (id: {})".format( style('project', frame.project), @@ -152,6 +157,10 @@ def stop(watson): style('time', frame.start.humanize()), style('short_id', frame.id) )) + + if frame.message is not None: + click.echo("Log message: {}".format(style('message', frame.message))) + watson.save() @@ -615,6 +624,9 @@ def edit(watson, id): if id: data['stop'] = frame.stop.format(datetime_format) + if frame.message is not None: + data['message'] = frame.message + text = json.dumps(data, indent=4, sort_keys=True, ensure_ascii=False) output = click.edit(text, extension='.json') @@ -630,6 +642,7 @@ def edit(watson, id): tzinfo=local_tz).to('utc') stop = arrow.get(data['stop'], datetime_format).replace( tzinfo=local_tz).to('utc') if id else None + message = data.get('message') except (ValueError, RuntimeError) as e: raise click.ClickException("Error saving edited frame: {}".format(e)) except KeyError: @@ -638,9 +651,17 @@ def edit(watson, id): ) if id: - watson.frames[id] = (project, start, stop, tags) + if all((project == frame.project, start == frame.start, + stop == frame.stop, tags == frame.tags, + message == frame.message)): + updated_at = frame.updated_at + else: + updated_at = arrow.utcnow() + + watson.frames[id] = (project, start, stop, tags, updated_at, message) else: - watson.current = dict(start=start, project=project, tags=tags) + watson.current = dict(start=start, project=project, tags=tags, + message=message) watson.save() click.echo( @@ -660,6 +681,9 @@ def edit(watson, id): ) ) + if message is not None: + click.echo("Log message: {}".format(style('message', message))) + @cli.command(context_settings={'ignore_unknown_options': True}) @click.argument('id') @@ -886,7 +910,8 @@ def merge(watson, frames_with_conflict, force): 'project': original_frame.project, 'start': original_frame.start.format(date_format), 'stop': original_frame.stop.format(date_format), - 'tags': original_frame.tags + 'tags': original_frame.tags, + 'message': original_frame.message } click.echo("frame {}:".format(style('short_id', original_frame.id))) click.echo("{}".format('\n'.join('<' + line for line in json.dumps( @@ -918,7 +943,8 @@ def merge(watson, frames_with_conflict, force): 'project': conflict_frame_copy.project, 'start': conflict_frame_copy.start.format(date_format), 'stop': conflict_frame_copy.stop.format(date_format), - 'tags': conflict_frame_copy.tags + 'tags': conflict_frame_copy.tags, + 'message': conflict_frame_copy.message } click.echo("{}".format('\n'.join('>' + line for line in json.dumps( conflict_frame_data, indent=4, ensure_ascii=False).splitlines()))) @@ -932,9 +958,9 @@ def merge(watson, frames_with_conflict, force): # merge in any non-conflicting frames for frame in merging: - id, project, start, stop, tags, updated_at = frame.dump() + id, project, start, stop, tags, updated_at, message = frame.dump() original_frames.add(project, start, stop, tags=tags, - updated_at=updated_at, id=id) + updated_at=updated_at, message=message, id=id) watson.frames.changed = True watson.save() diff --git a/watson/frames.py b/watson/frames.py index 77a085bb..aaa6442a 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -6,11 +6,12 @@ from collections import OrderedDict, namedtuple -FIELDS = ('id', 'project', 'start', 'stop', 'tags', 'updated_at') +FIELDS = ('id', 'project', 'start', 'stop', 'tags', 'updated_at', 'message') class Frame(namedtuple('Frame', FIELDS)): - def __new__(cls, id, project, start, stop, tags=None, updated_at=None): + def __new__(cls, id, project, start, stop, tags=None, updated_at=None, + message=None): try: if not isinstance(start, arrow.Arrow): start = arrow.get(start) @@ -33,7 +34,7 @@ def __new__(cls, id, project, start, stop, tags=None, updated_at=None): tags = [] return super(Frame, cls).__new__( - cls, id, project, start, stop, tags, updated_at + cls, id, project, start, stop, tags, updated_at, message ) def dump(self): @@ -41,7 +42,8 @@ def dump(self): stop = self.stop.to('utc').timestamp updated_at = self.updated_at.timestamp - return (self.id, self.project, start, stop, self.tags, updated_at) + return (self.id, self.project, start, stop, self.tags, updated_at, + self.message) @property def day(self): @@ -103,9 +105,10 @@ def __getitem__(self, id): def __setitem__(self, key, value): if isinstance(value, Frame): frame = self.new_frame(value.project, value.start, value.stop, - value.tags, value.updated_at, id=key) + value.tags, value.updated_at, value.message, + id=key) else: - frame = self.new_frame(*value[:5], id=key) + frame = self.new_frame(*value[:6], id=key) if key not in self: self._keys.append(key) @@ -129,11 +132,11 @@ def add(self, *args, **kwargs): return frame def new_frame(self, project, start, stop, tags=None, updated_at=None, - id=None): + message=None, id=None): if id is None: id = uuid.uuid4().hex - return Frame(id, project, start, stop, tags, updated_at) + return Frame(id, project, start, stop, tags, updated_at, message) def dump(self): return tuple(frame.dump() for frame in self.values()) diff --git a/watson/utils.py b/watson/utils.py index da04214b..c5e5d72f 100644 --- a/watson/utils.py +++ b/watson/utils.py @@ -25,7 +25,8 @@ def _style_short_id(id): 'error': {'fg': 'red'}, 'date': {'fg': 'cyan'}, 'short_id': _style_short_id, - 'id': {'fg': 'white'} + 'id': {'fg': 'white'}, + 'message': {'fg': 'white'}, } fmt = formats.get(name, {}) diff --git a/watson/watson.py b/watson/watson.py index c7a64407..75830931 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -151,6 +151,7 @@ def save(self): 'project': self.current['project'], 'start': self._format_date(self.current['start']), 'tags': self.current['tags'], + 'message': self.current.get('message'), } else: current = {} @@ -217,7 +218,8 @@ def current(self, value): self._current = { 'project': value['project'], 'start': start, - 'tags': value.get('tags') or [] + 'tags': value.get('tags') or [], + 'message': value.get('message'), } if self._old_state is None: @@ -266,9 +268,8 @@ def stop(self): raise WatsonError("No project started.") old = self.current - frame = self.frames.add( - old['project'], old['start'], arrow.now(), tags=old['tags'] - ) + frame = self.frames.add(old['project'], old['start'], arrow.now(), + tags=old['tags'], message=old.get('message')) self.current = None return frame From 0fff33d0a9a117b0fb905ae7cae0f60e348343bb Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 26 May 2016 23:14:48 +0200 Subject: [PATCH 13/49] Add tests for log messages Signed-off-by: Christopher Arndt --- tests/test_watson.py | 71 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/test_watson.py b/tests/test_watson.py index 2b783104..2e512c1c 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -348,6 +348,52 @@ def test_frames_without_tags(watson): assert frame.tags == [] +def test_frames_with_message(watson): + content = json.dumps([ + ['abcdefg', 'foo', 0, 10, ['A', 'B', 'C'], 30, + "My hovercraft is full of eels"] + ]) + + with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): + assert len(watson.frames) == 1 + frame = watson.frames['abcdefg'] + assert frame.id == 'abcdefg' + assert frame.project == 'foo' + assert frame.start == arrow.get(0) + assert frame.stop == arrow.get(10) + assert frame.tags == ['A', 'B', 'C'] + assert frame.message == "My hovercraft is full of eels" + + +def test_frames_without_message(watson): + content = json.dumps([ + ['abcdefg', 'foo', 0, 10], + ['hijklmn', 'foo', 10, 20, ['A', 'B', 'C']], + ['opqrstu', 'foo', 20, 30, ['A', 'B', 'C'], 30] + ]) + + with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): + assert len(watson.frames) == 3 + frame = watson.frames['abcdefg'] + assert frame.id == 'abcdefg' + assert frame.project == 'foo' + assert frame.start == arrow.get(0) + assert frame.stop == arrow.get(10) + assert frame.tags == [] + assert frame.message is None + + frame = watson.frames['hijklmn'] + assert frame.id == 'hijklmn' + assert frame.tags == ['A', 'B', 'C'] + assert frame.message is None + + frame = watson.frames['opqrstu'] + assert frame.id == 'opqrstu' + assert frame.tags == ['A', 'B', 'C'] + assert frame.updated_at == arrow.get(30) + assert frame.message is None + + def test_frames_with_empty_file(watson): with mock.patch('%s.open' % builtins, mock.mock_open(read_data="")): with mock.patch('os.path.getsize', return_value=0): @@ -572,6 +618,31 @@ def test_stop_started_project_without_tags(watson): assert frame.tags == [] +def test_stop_started_project_without_message(watson): + watson.start('foo') + watson.stop() + + assert watson.current == {} + assert watson.is_started is False + assert len(watson.frames) == 1 + frame = watson.frames.get_by_index(0) + assert frame.project == 'foo' + assert frame.message is None + + +def test_stop_started_project_with_message(watson): + watson.start('foo') + watson._current['message'] = "My hovercraft is full of eels" + watson.stop() + + assert watson.current == {} + assert watson.is_started is False + assert len(watson.frames) == 1 + frame = watson.frames.get_by_index(0) + assert frame.project == 'foo' + assert frame.message == "My hovercraft is full of eels" + + def test_stop_no_project(watson): with pytest.raises(WatsonError): watson.stop() From 5a9578d0fa687c065678ef76913d0c4e6b6328cf Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Thu, 9 Jun 2016 22:13:25 +0200 Subject: [PATCH 14/49] Add options for stop command to bash completion config Signed-off-by: Christopher Arndt --- watson.completion | 3 +++ 1 file changed, 3 insertions(+) diff --git a/watson.completion b/watson.completion index 5ccad5c3..3b13035e 100644 --- a/watson.completion +++ b/watson.completion @@ -76,6 +76,9 @@ _watson_complete () { COMPREPLY=($(compgen -W "$tags" -- ${cur})) fi ;; + stop) + COMPREPLY=($(compgen -W "-m --message" -- ${cur})) + ;; esac ;; esac From 9b8458104a3346b9914060317e7e8854cfe0b346 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Sun, 12 Jun 2016 11:29:00 +0200 Subject: [PATCH 15/49] Update documentation for stop command Signed-off-by: Christopher Arndt --- docs/user-guide/commands.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/user-guide/commands.md b/docs/user-guide/commands.md index 2832bcf2..84e8db5e 100644 --- a/docs/user-guide/commands.md +++ b/docs/user-guide/commands.md @@ -456,6 +456,7 @@ Example: Flag | Help -----|----- +`-m, --message TEXT` | Save given log message with the project frame. `--help` | Show this message and exit. ## `sync` From c369bb7cdd5a082d31ca2895dc362f1ddb06d392 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Wed, 15 Jun 2016 22:11:40 +0200 Subject: [PATCH 16/49] Enhanced stop command help Signed-off-by: Christopher Arndt --- watson/cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/watson/cli.py b/watson/cli.py index 3c73a3d8..932c9b90 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -141,11 +141,15 @@ def stop(watson, message): """ Stop monitoring time for the current project. + You can optionally pass a log message to be saved with the frame via + the ``-m/--message`` option. + Example: \b - $ watson stop + $ watson stop -m "Done some thinking" Stopping project apollo11, started a minute ago. (id: e7ccd52) + Log message: Done some thinking """ if watson.is_started and message is not None: watson._current['message'] = message From 67db7eef46f997676b4efb7a965e26122ca2de24 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Wed, 15 Jun 2016 22:14:33 +0200 Subject: [PATCH 17/49] Updated user guide Signed-off-by: Christopher Arndt --- docs/user-guide/commands.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/user-guide/commands.md b/docs/user-guide/commands.md index 84e8db5e..17c07518 100644 --- a/docs/user-guide/commands.md +++ b/docs/user-guide/commands.md @@ -446,11 +446,15 @@ Usage: watson stop [OPTIONS] Stop monitoring time for the current project. +You can optionally pass a log message to be saved with the frame via +the ``-m/--message`` option. + Example: - $ watson stop + $ watson stop -m "Done some thinking" Stopping project apollo11, started a minute ago. (id: e7ccd52) + Log message: Done some thinking ### Options From c5b11b56319e560b675377fb461a818befb87e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 4 Sep 2018 20:41:42 -0400 Subject: [PATCH 18/49] Revert "Add script to convert between old and new frame format (for development)" This reverts commit c7c3c84f8756a9861f7ec520db8906f273e83a25. --- scripts/conv_frames.py | 48 ------------------------------------------ 1 file changed, 48 deletions(-) delete mode 100755 scripts/conv_frames.py diff --git a/scripts/conv_frames.py b/scripts/conv_frames.py deleted file mode 100755 index 443c3621..00000000 --- a/scripts/conv_frames.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -"""Convert the watson frames file from the old to new format and vice versa.""" - -from __future__ import print_function - -import json -import os -import tempfile - -import click - - -app_dir = click.get_app_dir('watson') -frames_file = os.path.join(app_dir, 'frames') -backup_file = frames_file + '.old' - -with open(frames_file) as fp: - frames = json.load(fp) - -if frames: - if isinstance(frames[0][0], int) and isinstance(frames[0][1], int): - print("Converting frames to new format...") - converted_frames = [[f[3], f[2], f[0], f[1], f[4], f[5]] - for f in frames] - else: - print("Converting frames to old format...") - converted_frames = [[f[2], f[3], f[1], f[0], f[4], f[5]] - for f in frames] - - try: - with tempfile.NamedTemporaryFile('w', delete=False) as tmpfp: - json.dump(converted_frames, tmpfp, indent=1, ensure_ascii=False) - - if os.path.exists(backup_file): - raise IOError("Backup file '{}' already exists. " - "Will not overwrite.".format(backup_file)) - else: - os.rename(frames_file, backup_file) - - os.rename(tmpfp.name, frames_file) - finally: - try: - os.unlink(tmpfp.name) - except: - pass -else: - print("No frames found. Nothing to do.") From 124dc00c2429bdbb5ae8c150f6382384526b706c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 4 Sep 2018 20:41:56 -0400 Subject: [PATCH 19/49] Revert "When looking up a frame by ID prefix, iterate over keys in reverse order for better perfomance in most likely case" This reverts commit 8d2206ff3f1eea4512df3896ff57e01549158998. --- tests/test_watson.py | 2 +- watson/frames.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index 2e512c1c..74763b78 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -330,7 +330,7 @@ def test_frames_get_by_id(watson): assert frame.project == 'project1' frame = watson.frames['abc'] - assert frame.project == 'project1' + assert frame.project == 'project0' frame = watson.frames['def'] assert frame.project == 'project2' diff --git a/watson/frames.py b/watson/frames.py index aaa6442a..a986e33a 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -96,7 +96,7 @@ def __getitem__(self, id): try: return super(Frames, self).__getitem__(id) except KeyError: - for key in reversed(self._keys): + for key in self._keys: if key.startswith(id): return super(Frames, self).__getitem__(key) else: From 51c49c95229a605ee27eb1c3327e808ffcaab0a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 4 Sep 2018 20:42:01 -0400 Subject: [PATCH 20/49] Revert "(Re-)add support for looking up frames by ID prefix" This reverts commit c956089e176a0c6792be4ebe927e5dbc605e85f7. --- tests/test_watson.py | 21 --------------------- watson/frames.py | 10 ---------- 2 files changed, 31 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index 74763b78..ebac036d 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -314,27 +314,6 @@ def test_frames_get_by_index(watson): assert watson.frames.get_by_index(0).project == 'project4' -def test_frames_get_by_id(watson): - test_frames = ( - ('abcdef', ('project0', 0, 10)), - ('abcxyz', ('project1', 10, 20)), - ('defghi', ('project2', 20, 30)), - ) - - for id, frame in test_frames: - watson.frames[id] = frame - - frame = watson.frames['abcdef'] - assert frame.project == 'project0' - frame = watson.frames['abcxyz'] - assert frame.project == 'project1' - - frame = watson.frames['abc'] - assert frame.project == 'project0' - frame = watson.frames['def'] - assert frame.project == 'project2' - - def test_frames_without_tags(watson): content = json.dumps([['abcdefg', 'foo', 0, 10]]) diff --git a/watson/frames.py b/watson/frames.py index a986e33a..2fa8b3ba 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -92,16 +92,6 @@ def __init__(self, frames=None): self.changed = False - def __getitem__(self, id): - try: - return super(Frames, self).__getitem__(id) - except KeyError: - for key in self._keys: - if key.startswith(id): - return super(Frames, self).__getitem__(key) - else: - raise KeyError("Frame with id {} not found.".format(id)) - def __setitem__(self, key, value): if isinstance(value, Frame): frame = self.new_frame(value.project, value.start, value.stop, From 075580e0f5836e10178fda692a450c76ab929d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 4 Sep 2018 20:42:05 -0400 Subject: [PATCH 21/49] Revert "Add more tests for setting watson.frames" This reverts commit 73c25931b40b6ebca294219eb46354153e585485. --- tests/test_watson.py | 101 ------------------------------------------- 1 file changed, 101 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index ebac036d..e20cee2e 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -19,7 +19,6 @@ from click import get_app_dir from watson import Watson, WatsonError -from watson.frames import Frame, Frames from watson.watson import ConfigurationError, ConfigParser TEST_FIXTURE_DIR = py.path.local( @@ -160,106 +159,6 @@ def test_frames(watson): assert frame.tags == ['A', 'B', 'C'] -def test_frames_from_sequence_of_tuples(watson): - test_frames = ( - ('0', 'project0', 0, 10), - ('1', 'project1', 10, 20, ['A', 'B', 'C']) - ) - watson.frames = test_frames - - assert len(watson.frames) == 2 - assert isinstance(watson.frames, Frames) - - frame = watson.frames.get_by_index(0) - assert isinstance(frame, Frame) - assert frame.id == '0' - assert frame.project == 'project0' - assert frame.start == arrow.get(0) - assert frame.stop == arrow.get(10) - - frame = watson.frames.get_by_index(1) - assert isinstance(frame, Frame) - assert frame.id == '1' - assert frame.project == 'project1' - assert frame.start == arrow.get(10) - assert frame.stop == arrow.get(20) - assert frame.tags == ['A', 'B', 'C'] - - -def test_frames_from_sequence_of_lists(watson): - test_frames = ( - ['0', 'project0', 0, 10], - ['1', 'project1', 10, 20, ['A', 'B', 'C']] - ) - watson.frames = test_frames - - assert len(watson.frames) == 2 - assert isinstance(watson.frames, Frames) - - frame = watson.frames.get_by_index(0) - assert isinstance(frame, Frame) - assert frame.id == '0' - assert frame.project == 'project0' - assert frame.start == arrow.get(0) - assert frame.stop == arrow.get(10) - - frame = watson.frames.get_by_index(1) - assert isinstance(frame, Frame) - assert frame.id == '1' - assert frame.project == 'project1' - assert frame.start == arrow.get(10) - assert frame.stop == arrow.get(20) - assert frame.tags == ['A', 'B', 'C'] - - -def test_frames_from_old_format(watson): - test_frames = ( - [0, 10, 'project0', '0'], - [10, 20, 'project1', '1', ['A', 'B', 'C']], - [20, 30, 'project2', '2', ['D', 'E', 'F'], 30] - ) - watson.frames = test_frames - - assert len(watson.frames) == 3 - assert isinstance(watson.frames, Frames) - - frame = watson.frames.get_by_index(0) - assert isinstance(frame, Frame) - assert frame.id == '0' - assert frame.project == 'project0' - assert frame.start == arrow.get(0) - assert frame.stop == arrow.get(10) - - frame = watson.frames.get_by_index(1) - assert isinstance(frame, Frame) - assert frame.id == '1' - assert frame.project == 'project1' - assert frame.start == arrow.get(10) - assert frame.stop == arrow.get(20) - assert frame.tags == ['A', 'B', 'C'] - - frame = watson.frames.get_by_index(2) - assert isinstance(frame, Frame) - assert frame.id == '2' - assert frame.project == 'project2' - assert frame.start == arrow.get(20) - assert frame.stop == arrow.get(30) - assert frame.tags == ['D', 'E', 'F'] - assert frame.updated_at == arrow.get(30) - - -def test_frames_from_frames(watson): - frames_instance = Frames(( - ('0', 'project0', 0, 10), - ('1', 'project1', 10, 20, ['A', 'B', 'C']) - )) - watson.frames = frames_instance - - assert len(watson.frames) == 2 - assert isinstance(watson.frames, Frames) - assert watson.frames is frames_instance - - def test_frames_get_by_index(watson): test_frames = ( ('0', ('project0', 0, 10)), From 39f966eda5364fd6537baf51d7e0ed74cafb1477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 4 Sep 2018 20:42:41 -0400 Subject: [PATCH 22/49] Revert "Make sure passing a sequence of frames works with tuple and list items" This reverts commit 714b58159b89f56cf301d5ac429c7c6b32a16ba7. --- watson/frames.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/watson/frames.py b/watson/frames.py index 2fa8b3ba..62920ced 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -80,12 +80,7 @@ def __init__(self, frames=None): for frame in frames or []: # convert from old format with project @ idx 2 and ID @ idx 3 if not isinstance(frame[2], (int, float)): - frame = ( - frame[3], # id - frame[2], # project - frame[0], # start - frame[1] # stop - ) + tuple(frame[4:]) + frame = [frame[3], frame[2], frame[0], frame[1]] + frame[4:] frame = Frame(*frame) self[frame.id] = frame From 0b3673a6b796b1649f9d3b69b191611172b0c1a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 4 Sep 2018 20:42:45 -0400 Subject: [PATCH 23/49] Revert "Assigning a Frames instance to watson.frames should work with new Frames format too; Clarify docstring on passing frames to construcutor as well" This reverts commit 553e34a73799d5e94941a84da94337d244ddaf53. --- watson/watson.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/watson/watson.py b/watson/watson.py index 75830931..ebc31ec3 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -29,17 +29,10 @@ class ConfigurationError(WatsonError, configparser.Error): class Watson(object): def __init__(self, **kwargs): """ - :param frames: If given, should be a sequence of frames or a - frames.Frames instance. - - If a sequence is given, each item may either be a - frames.Frame instance or a sequence of frame values, - with at least these 4 items: - - (id, project, start, stop) - - If not given, the value is extracted from the frames - file. + :param frames: If given, should be a list representating the + frames. + If not given, the value is extracted + from the frames file. :type frames: list :param current: If given, should be a dict representating the @@ -185,10 +178,7 @@ def frames(self): @frames.setter def frames(self, frames): - if isinstance(frames, Frames): - self._frames = frames - else: - self._frames = Frames(frames) + self._frames = Frames(frames) @property def current(self): From aa075b3ff61cc6fa0031e027087e30b10fdff5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 4 Sep 2018 20:42:48 -0400 Subject: [PATCH 24/49] Revert "Remove uneccessary assignment since both names already refer to the same object" This reverts commit 8ed29c8f82ce7734796d1997736ad75eedf5a5f1. --- watson/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/watson/cli.py b/watson/cli.py index 932c9b90..45d26aab 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -966,5 +966,6 @@ def merge(watson, frames_with_conflict, force): original_frames.add(project, start, stop, tags=tags, updated_at=updated_at, message=message, id=id) + watson.frames = original_frames watson.frames.changed = True watson.save() From 8f98674544abb28e1223d1b323ecd321051ff779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 4 Sep 2018 20:44:30 -0400 Subject: [PATCH 25/49] Merge branch 'feature/refactor-frames' into feature/log-message Signed-off-by: Christopher Arndt Merge branch 'feature/log-message' of github.com:SpotlightKid/Watson into feature/log-message Signed-off-by: Christopher Arndt --- watson/cli.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index 45d26aab..a44028ee 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -962,10 +962,9 @@ def merge(watson, frames_with_conflict, force): # merge in any non-conflicting frames for frame in merging: - id, project, start, stop, tags, updated_at, message = frame.dump() - original_frames.add(project, start, stop, tags=tags, - updated_at=updated_at, message=message, id=id) - + start, stop, project, id, tags, updated_at, message = frame.dump() + original_frames.add(project, start, stop, tags=tags, id=id, + updated_at=updated_at, message=message) watson.frames = original_frames watson.frames.changed = True watson.save() From a09ada7e8047c41a05b86bf8701c04bdbda51617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 4 Sep 2018 20:44:55 -0400 Subject: [PATCH 26/49] Revert "Optimize index access to Frames and add tests for 'get_by_index'" This reverts commit a1a905a336fd05d4c99d613509e001c602ea73a3. --- tests/test_watson.py | 60 +++----------------------------------------- watson/frames.py | 14 ++--------- 2 files changed, 5 insertions(+), 69 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index e20cee2e..63293dab 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -151,7 +151,7 @@ def test_frames(watson): with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): assert len(watson.frames) == 1 - frame = watson.frames['abcdefg'] + frame = watson.frames.get_by_index(0) assert frame.id == 'abcdefg' assert frame.project == 'foo' assert frame.start == arrow.get(0) @@ -159,66 +159,12 @@ def test_frames(watson): assert frame.tags == ['A', 'B', 'C'] -def test_frames_get_by_index(watson): - test_frames = ( - ('0', ('project0', 0, 10)), - ('1', ('project1', 10, 20)), - ('2', ('project2', 20, 30)), - ('3', ('project3', 30, 40)), - ('4', ('project4', 40, 50)) - ) - - for id, frame in test_frames[:-1]: - watson.frames[id] = frame - - assert len(watson.frames) == 4 - assert watson.frames.get_by_index(0).id == '0' - assert watson.frames.get_by_index(0).project == 'project0' - assert watson.frames.get_by_index(2).id == '2' - assert watson.frames.get_by_index(2).project == 'project2' - assert watson.frames.get_by_index(-1).id == '3' - assert watson.frames.get_by_index(-1).project == 'project3' - - # adding an item - id, frame = test_frames[-1] - watson.frames[id] = frame - assert len(watson.frames) == 5 - assert watson.frames.get_by_index(3).id == '3' - assert watson.frames.get_by_index(-1).id == '4' - - # setting an existing item - assert watson.frames.get_by_index(2).project == 'project2' - watson.frames['2'] = ('project6', 50, 60) - assert len(watson.frames) == 5 - assert watson.frames.get_by_index(2).project == 'project6' - - # deleting an item - del watson.frames['2'] - assert len(watson.frames) == 4 - assert watson.frames.get_by_index(2).id == '3' - assert watson.frames.get_by_index(2).project == 'project3' - - # index out of range - with pytest.raises(IndexError): - watson.frames.get_by_index(4) - - if not PY2: - # move_to_end - assert watson.frames.get_by_index(0).project == 'project0' - watson.frames.move_to_end('0') - assert watson.frames.get_by_index(-1).project == 'project0' - - assert watson.frames.get_by_index(0).project == 'project1' - watson.frames.move_to_end('4', False) - assert watson.frames.get_by_index(0).project == 'project4' - - def test_frames_without_tags(watson): content = json.dumps([['abcdefg', 'foo', 0, 10]]) with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): assert len(watson.frames) == 1 - frame = watson.frames['abcdefg'] + frame = watson.frames.get_by_index(0) assert frame.id == 'abcdefg' assert frame.project == 'foo' assert frame.start == arrow.get(0) @@ -300,7 +246,7 @@ def test_given_frames(config_dir): with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): assert len(watson.frames) == 1 - frame = watson.frames['abcdefg'] + frame = watson.frames.get_by_index(0) assert frame.project == 'bar' assert frame.tags == ['A', 'B'] diff --git a/watson/frames.py b/watson/frames.py index 62920ced..aa210bbf 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -75,8 +75,6 @@ def __contains__(self, frame): class Frames(OrderedDict): def __init__(self, frames=None): super(Frames, self).__init__() - self._keys = list(self.keys()) - for frame in frames or []: # convert from old format with project @ idx 2 and ID @ idx 3 if not isinstance(frame[2], (int, float)): @@ -95,22 +93,13 @@ def __setitem__(self, key, value): else: frame = self.new_frame(*value[:6], id=key) - if key not in self: - self._keys.append(key) - super(Frames, self).__setitem__(key, frame) self.changed = True def __delitem__(self, key): super(Frames, self).__delitem__(key) - self._keys.remove(key) self.changed = True - def move_to_end(self, key, last=True): - super(Frames, self).move_to_end(key, last) - self._keys.remove(key) - self._keys.insert(len(self._keys) if last else 0, key) - def add(self, *args, **kwargs): frame = self.new_frame(*args, **kwargs) self[frame.id] = frame @@ -134,7 +123,8 @@ def filter(self, projects=None, tags=None, span=None): yield frame def get_by_index(self, index): - return self[self._keys[index]] + key = list(self.keys())[index] + return self[key] def get_column(self, col): index = FIELDS.index(col) From 6463412f255b843dfcdaa3d1c56fcd12604c7534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 4 Sep 2018 20:45:00 -0400 Subject: [PATCH 27/49] Revert "Fix flake8 complaint by refactoring boolean expr" This reverts commit 92b8733b66c41ef4d8bb5687994d979b28c1d6d3. --- watson/frames.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watson/frames.py b/watson/frames.py index aa210bbf..eaa47dd6 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -118,8 +118,8 @@ def dump(self): def filter(self, projects=None, tags=None, span=None): for frame in self.values(): if ((projects is None or frame.project in projects) and - (tags is None or set(frame.tags).intersection(tags)) and - (span is None or frame in span)): + (tags is None or any(tag in frame.tags for tag in tags)) and + (span is None or frame in span)): yield frame def get_by_index(self, index): From 4628f94a61fe7bc4e097237450fe8e7ea4854479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 4 Sep 2018 20:56:50 -0400 Subject: [PATCH 28/49] Revert "Refactor 'watson.frames'" --- tests/resources/frames-with-conflict | 36 ++++----- tests/test_watson.py | 101 ++++++++++-------------- watson/cli.py | 6 +- watson/frames.py | 111 ++++++++++++++++----------- watson/utils.py | 2 +- watson/watson.py | 10 +-- 6 files changed, 134 insertions(+), 132 deletions(-) diff --git a/tests/resources/frames-with-conflict b/tests/resources/frames-with-conflict index 0a01a81d..53998ea1 100644 --- a/tests/resources/frames-with-conflict +++ b/tests/resources/frames-with-conflict @@ -1,26 +1,26 @@ [ [ - "1", - "foo", - 0, - 15, - [], + 0, + 15, + "foo", + "1", + [], 15 - ], + ], [ - "2", - "bar", - 20, - 50, - [], + 20, + 50, + "bar", + "2", + [], 50 - ], + ], [ - "3", - "bar", - 50, - 100, - [], + 50, + 100, + "bar", + "3", + [], 100 ] -] +] \ No newline at end of file diff --git a/tests/test_watson.py b/tests/test_watson.py index 63293dab..e4998b67 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -147,29 +147,25 @@ def test_last_sync_with_empty_given_state(config_dir): # frames def test_frames(watson): - content = json.dumps([['abcdefg', 'foo', 0, 10, ['A', 'B', 'C']]]) + content = json.dumps([[0, 10, 'foo', None, ['A', 'B', 'C']]]) with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): assert len(watson.frames) == 1 - frame = watson.frames.get_by_index(0) - assert frame.id == 'abcdefg' - assert frame.project == 'foo' - assert frame.start == arrow.get(0) - assert frame.stop == arrow.get(10) - assert frame.tags == ['A', 'B', 'C'] + assert watson.frames[0].project == 'foo' + assert watson.frames[0].start == arrow.get(0) + assert watson.frames[0].stop == arrow.get(10) + assert watson.frames[0].tags == ['A', 'B', 'C'] def test_frames_without_tags(watson): - content = json.dumps([['abcdefg', 'foo', 0, 10]]) + content = json.dumps([[0, 10, 'foo', None]]) with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): assert len(watson.frames) == 1 - frame = watson.frames.get_by_index(0) - assert frame.id == 'abcdefg' - assert frame.project == 'foo' - assert frame.start == arrow.get(0) - assert frame.stop == arrow.get(10) - assert frame.tags == [] + assert watson.frames[0].project == 'foo' + assert watson.frames[0].start == arrow.get(0) + assert watson.frames[0].stop == arrow.get(10) + assert watson.frames[0].tags == [] def test_frames_with_message(watson): @@ -240,19 +236,18 @@ def test_frames_watson_non_valid_json(watson): def test_given_frames(config_dir): - content = json.dumps([['abcdefg', 'foo', 0, 10, ['A']]]) - watson = Watson(frames=[['abcdefg', 'bar', 0, 10, ['A', 'B']]], + content = json.dumps([[0, 10, 'foo', None, ['A']]]) + watson = Watson(frames=[[0, 10, 'bar', None, ['A', 'B']]], config_dir=config_dir) with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): assert len(watson.frames) == 1 - frame = watson.frames.get_by_index(0) - assert frame.project == 'bar' - assert frame.tags == ['A', 'B'] + assert watson.frames[0].project == 'bar' + assert watson.frames[0].tags == ['A', 'B'] def test_frames_with_empty_given_state(config_dir): - content = json.dumps([['abcdefg', 'foo', 0, 10, ['A']]]) + content = json.dumps([[0, 10, 'foo', None, ['A']]]) watson = Watson(frames=[], config_dir=config_dir) with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): @@ -421,11 +416,10 @@ def test_stop_started_project(watson): assert watson.current == {} assert watson.is_started is False assert len(watson.frames) == 1 - frame = watson.frames.get_by_index(0) - assert frame.project == 'foo' - assert isinstance(frame.start, arrow.Arrow) - assert isinstance(frame.stop, arrow.Arrow) - assert frame.tags == ['A', 'B'] + assert watson.frames[0].project == 'foo' + assert isinstance(watson.frames[0].start, arrow.Arrow) + assert isinstance(watson.frames[0].stop, arrow.Arrow) + assert watson.frames[0].tags == ['A', 'B'] def test_stop_started_project_without_tags(watson): @@ -435,11 +429,10 @@ def test_stop_started_project_without_tags(watson): assert watson.current == {} assert watson.is_started is False assert len(watson.frames) == 1 - frame = watson.frames.get_by_index(0) - assert frame.project == 'foo' - assert isinstance(frame.start, arrow.Arrow) - assert isinstance(frame.stop, arrow.Arrow) - assert frame.tags == [] + assert watson.frames[0].project == 'foo' + assert isinstance(watson.frames[0].start, arrow.Arrow) + assert isinstance(watson.frames[0].stop, arrow.Arrow) + assert watson.frames[0].tags == [] def test_stop_started_project_without_message(watson): @@ -543,7 +536,7 @@ def test_save_empty_current(config_dir): def test_save_frames_no_change(config_dir): - watson = Watson(frames=[['abcdefg', 'foo', 0, 10]], + watson = Watson(frames=[[0, 10, 'foo', None]], config_dir=config_dir) with mock.patch('%s.open' % builtins, mock.mock_open()): @@ -554,8 +547,8 @@ def test_save_frames_no_change(config_dir): def test_save_added_frame(config_dir): - watson = Watson(frames=[['abcdefg', 'foo', 0, 10]], config_dir=config_dir) - watson.frames.add('bar', 10, 20, tags=['A']) + watson = Watson(frames=[[0, 10, 'foo', None]], config_dir=config_dir) + watson.frames.add('bar', 10, 20, ['A']) with mock.patch('%s.open' % builtins, mock.mock_open()): with mock.patch('json.dump') as json_mock: @@ -564,21 +557,16 @@ def test_save_added_frame(config_dir): assert json_mock.call_count == 1 result = json_mock.call_args[0][0] assert len(result) == 2 - assert result[0][0] == 'abcdefg' - assert result[0][1] == 'foo' - assert result[0][2] == 0 - assert result[0][3] == 10 + assert result[0][2] == 'foo' assert result[0][4] == [] - assert result[1][1] == 'bar' - assert result[1][2] == 10 - assert result[1][3] == 20 + assert result[1][2] == 'bar' assert result[1][4] == ['A'] def test_save_changed_frame(config_dir): - watson = Watson(frames=[['abcdefg', 'foo', 0, 10, ['A']]], + watson = Watson(frames=[[0, 10, 'foo', None, ['A']]], config_dir=config_dir) - watson.frames['abcdefg'] = ('bar', 0, 10, ['A', 'B']) + watson.frames[0] = ('bar', 0, 10, ['A', 'B']) with mock.patch('%s.open' % builtins, mock.mock_open()): with mock.patch('json.dump') as json_mock: @@ -587,10 +575,7 @@ def test_save_changed_frame(config_dir): assert json_mock.call_count == 1 result = json_mock.call_args[0][0] assert len(result) == 1 - assert result[0][0] == 'abcdefg' - assert result[0][1] == 'bar' - assert result[0][2] == 0 - assert result[0][3] == 10 + assert result[0][2] == 'bar' assert result[0][4] == ['A', 'B'] dump_args = json_mock.call_args[1] @@ -802,19 +787,17 @@ def json(self): assert len(watson.frames) == 2 - frame = watson.frames.get_by_index(0) - assert frame.id == '1' - assert frame.project == 'foo' - assert frame.start.timestamp == 3 - assert frame.stop.timestamp == 4 - assert frame.tags == ['A'] - - frame = watson.frames.get_by_index(1) - assert frame.id == '2' - assert frame.project == 'bar' - assert frame.start.timestamp == 4 - assert frame.stop.timestamp == 5 - assert frame.tags == [] + assert watson.frames[0].id == '1' + assert watson.frames[0].project == 'foo' + assert watson.frames[0].start.timestamp == 3 + assert watson.frames[0].stop.timestamp == 4 + assert watson.frames[0].tags == ['A'] + + assert watson.frames[1].id == '2' + assert watson.frames[1].project == 'bar' + assert watson.frames[1].start.timestamp == 4 + assert watson.frames[1].stop.timestamp == 5 + assert watson.frames[1].tags == [] # projects diff --git a/watson/cli.py b/watson/cli.py index a44028ee..71d2327a 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -581,7 +581,7 @@ def frames(watson): [...] """ for frame in watson.frames: - click.echo(style('short_id', frame)) + click.echo(style('short_id', frame.id)) @cli.command(context_settings={'ignore_unknown_options': True}) @@ -609,10 +609,10 @@ def edit(watson, id): frame = get_frame_from_argument(watson, id) id = frame.id elif watson.is_started: - frame = Frame(None, watson.current['project'], watson.current['start'], + frame = Frame(watson.current['start'], None, watson.current['project'], None, watson.current['tags']) elif watson.frames: - frame = watson.frames.get_by_index(-1) + frame = watson.frames[-1] id = frame.id else: raise click.ClickException( diff --git a/watson/frames.py b/watson/frames.py index eaa47dd6..776f3598 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -1,16 +1,14 @@ -# -*- coding: utf-8 -*- - import uuid import arrow -from collections import OrderedDict, namedtuple +from collections import namedtuple -FIELDS = ('id', 'project', 'start', 'stop', 'tags', 'updated_at', 'message') +HEADERS = ('start', 'stop', 'project', 'id', 'tags', 'updated_at', 'message') -class Frame(namedtuple('Frame', FIELDS)): - def __new__(cls, id, project, start, stop, tags=None, updated_at=None, +class Frame(namedtuple('Frame', HEADERS)): + def __new__(cls, start, stop, project, id, tags=None, updated_at=None, message=None): try: if not isinstance(start, arrow.Arrow): @@ -34,15 +32,14 @@ def __new__(cls, id, project, start, stop, tags=None, updated_at=None, tags = [] return super(Frame, cls).__new__( - cls, id, project, start, stop, tags, updated_at, message - ) + cls, start, stop, project, id, tags, updated_at, message def dump(self): start = self.start.to('utc').timestamp stop = self.stop.to('utc').timestamp updated_at = self.updated_at.timestamp - return (self.id, self.project, start, stop, self.tags, updated_at, + return (start, stop, self.project, self.id, self.tags, updated_at, self.message) @property @@ -72,64 +69,88 @@ def __contains__(self, frame): return frame.start >= self.start and frame.stop <= self.stop -class Frames(OrderedDict): +class Frames(object): def __init__(self, frames=None): - super(Frames, self).__init__() - for frame in frames or []: - # convert from old format with project @ idx 2 and ID @ idx 3 - if not isinstance(frame[2], (int, float)): - frame = [frame[3], frame[2], frame[0], frame[1]] + frame[4:] + if not frames: + frames = [] - frame = Frame(*frame) - self[frame.id] = frame + rows = [Frame(*frame) for frame in frames] + self._rows = rows self.changed = False + def __len__(self): + return len(self._rows) + + def __getitem__(self, key): + if key in HEADERS: + return tuple(self._get_col(key)) + elif isinstance(key, int): + return self._rows[key] + else: + return self._rows[self._get_index_by_id(key)] + def __setitem__(self, key, value): + self.changed = True + if isinstance(value, Frame): - frame = self.new_frame(value.project, value.start, value.stop, - value.tags, value.updated_at, value.message, - id=key) + frame = value else: - frame = self.new_frame(*value[:6], id=key) + frame = self.new_frame(*value) - super(Frames, self).__setitem__(key, frame) - self.changed = True + if isinstance(key, int): + self._rows[key] = frame + else: + frame = frame._replace(id=key) + try: + self._rows[self._get_index_by_id(key)] = frame + except KeyError: + self._rows.append(frame) def __delitem__(self, key): - super(Frames, self).__delitem__(key) self.changed = True + if isinstance(key, int): + del self._rows[key] + else: + del self._rows[self._get_index_by_id(key)] + + def _get_index_by_id(self, id): + try: + return next( + i for i, v in enumerate(self['id']) if v.startswith(id) + ) + except StopIteration: + raise KeyError("Frame with id {} not found.".format(id)) + + def _get_col(self, col): + index = HEADERS.index(col) + for row in self._rows: + yield row[index] + def add(self, *args, **kwargs): + self.changed = True frame = self.new_frame(*args, **kwargs) - self[frame.id] = frame + self._rows.append(frame) return frame - def new_frame(self, project, start, stop, tags=None, updated_at=None, - message=None, id=None): - if id is None: + def new_frame(self, project, start, stop, tags=None, id=None, + updated_at=None, message=None): + if not id: id = uuid.uuid4().hex - - return Frame(id, project, start, stop, tags, updated_at, message) + return Frame(start, stop, project, id, tags=tags, + updated_at=updated_at, message=message) def dump(self): - return tuple(frame.dump() for frame in self.values()) + return tuple(frame.dump() for frame in self._rows) def filter(self, projects=None, tags=None, span=None): - for frame in self.values(): - if ((projects is None or frame.project in projects) and - (tags is None or any(tag in frame.tags for tag in tags)) and - (span is None or frame in span)): - yield frame - - def get_by_index(self, index): - key = list(self.keys())[index] - return self[key] - - def get_column(self, col): - index = FIELDS.index(col) - for row in self.values(): - yield row[index] + return ( + frame for frame in self._rows + if (projects is None or frame.project in projects) and + (tags is None or any(tag in frame.tags for tag in tags)) and + (span is None or frame in span) + ) def span(self, start, stop): return Span(start, stop) diff --git a/watson/utils.py b/watson/utils.py index c5e5d72f..a4d76e45 100644 --- a/watson/utils.py +++ b/watson/utils.py @@ -96,7 +96,7 @@ def get_frame_from_argument(watson, arg): try: index = int(arg) if index < 0: - return watson.frames.get_by_index(index) + return watson.frames[index] except IndexError: raise click.ClickException( style('error', "No frame found for index {}.".format(arg)) diff --git a/watson/watson.py b/watson/watson.py index ebc31ec3..7f45061e 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import os -import itertools import json try: @@ -277,15 +276,14 @@ def projects(self): """ Return the list of all the existing projects, sorted by name. """ - return sorted(set(self.frames.get_column('project'))) + return sorted(set(self.frames['project'])) @property def tags(self): """ Return the list of the tags, sorted by name. """ - return sorted( - set(itertools.chain.from_iterable(self.frames.get_column('tags')))) + return sorted(set(tag for tags in self.frames['tags'] for tag in tags)) def _get_request_info(self, route): config = self.config @@ -371,7 +369,7 @@ def push(self, last_pull): frames = [] - for frame in self.frames.values(): + for frame in self.frames: if last_pull > frame.updated_at > self.last_sync: try: # Find the url of the project @@ -414,7 +412,7 @@ def merge_report(self, frames_with_conflict): conflicting = [] merging = [] - for conflict_frame in conflict_file_frames.values(): + for conflict_frame in conflict_file_frames: try: original_frame = self.frames[conflict_frame.id] From 639553d729c97d9cb8d7d520b8439e5c178dd019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 4 Sep 2018 21:01:11 -0400 Subject: [PATCH 29/49] Add back a missing ) deleted by mistake --- watson/frames.py | 1 + 1 file changed, 1 insertion(+) diff --git a/watson/frames.py b/watson/frames.py index 776f3598..04b08e43 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -33,6 +33,7 @@ def __new__(cls, start, stop, project, id, tags=None, updated_at=None, return super(Frame, cls).__new__( cls, start, stop, project, id, tags, updated_at, message + ) def dump(self): start = self.start.to('utc').timestamp From 693d9d5493eb9aa3c23ca3c4adc2b0e854a880ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 8 Feb 2019 23:17:19 -0500 Subject: [PATCH 30/49] Fix test_frames_with_message --- tests/test_watson.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index 2085225d..604ff590 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -156,21 +156,22 @@ def test_frames_without_tags(mock, watson): assert watson.frames[0].tags == [] -def test_frames_with_message(mock, watson): +def test_frames_with_message(mocker, watson): + """Test loading frames with messages.""" content = json.dumps([ - ['abcdefg', 'foo', 0, 10, ['A', 'B', 'C'], 30, + [3601, 3610, 'foo', 'abcdefg', ['A', 'B', 'C'], 3650, "My hovercraft is full of eels"] ]) - - with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): - assert len(watson.frames) == 1 - frame = watson.frames['abcdefg'] - assert frame.id == 'abcdefg' - assert frame.project == 'foo' - assert frame.start == arrow.get(0) - assert frame.stop == arrow.get(10) - assert frame.tags == ['A', 'B', 'C'] - assert frame.message == "My hovercraft is full of eels" + + mocker.patch('%s.open' % builtins, mocker.mock_open(read_data=content)) + assert len(watson.frames) == 1 + frame = watson.frames['abcdefg'] + assert frame.id == 'abcdefg' + assert frame.project == 'foo' + assert frame.start == arrow.get(3601) + assert frame.stop == arrow.get(3610) + assert frame.tags == ['A', 'B', 'C'] + assert frame.message == "My hovercraft is full of eels" def test_frames_without_message(mock, watson): From dc9d2994ea1a9c1be93f92c63723282ec327d115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 8 Feb 2019 23:17:43 -0500 Subject: [PATCH 31/49] Fix test test_frames_without_message --- tests/test_watson.py | 49 ++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index 604ff590..0e5276e8 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -174,33 +174,34 @@ def test_frames_with_message(mocker, watson): assert frame.message == "My hovercraft is full of eels" -def test_frames_without_message(mock, watson): +def test_frames_without_message(mocker, watson): + """Test loading frames without messages.""" content = json.dumps([ - ['abcdefg', 'foo', 0, 10], - ['hijklmn', 'foo', 10, 20, ['A', 'B', 'C']], - ['opqrstu', 'foo', 20, 30, ['A', 'B', 'C'], 30] + [3601, 3610, 'foo', 'abcdefg'], + [3611, 3620, 'foo', 'hijklmn', ['A', 'B', 'C']], + [3621, 3630, 'foo', 'opqrstu', ['A', 'B', 'C'], 3630] ]) - with mock.patch('%s.open' % builtins, mock.mock_open(read_data=content)): - assert len(watson.frames) == 3 - frame = watson.frames['abcdefg'] - assert frame.id == 'abcdefg' - assert frame.project == 'foo' - assert frame.start == arrow.get(0) - assert frame.stop == arrow.get(10) - assert frame.tags == [] - assert frame.message is None - - frame = watson.frames['hijklmn'] - assert frame.id == 'hijklmn' - assert frame.tags == ['A', 'B', 'C'] - assert frame.message is None - - frame = watson.frames['opqrstu'] - assert frame.id == 'opqrstu' - assert frame.tags == ['A', 'B', 'C'] - assert frame.updated_at == arrow.get(30) - assert frame.message is None + mocker.patch('%s.open' % builtins, mocker.mock_open(read_data=content)) + assert len(watson.frames) == 3 + frame = watson.frames['abcdefg'] + assert frame.id == 'abcdefg' + assert frame.project == 'foo' + assert frame.start == arrow.get(3601) + assert frame.stop == arrow.get(3610) + assert frame.tags == [] + assert frame.message is None + + frame = watson.frames['hijklmn'] + assert frame.id == 'hijklmn' + assert frame.tags == ['A', 'B', 'C'] + assert frame.message is None + + frame = watson.frames['opqrstu'] + assert frame.id == 'opqrstu' + assert frame.tags == ['A', 'B', 'C'] + assert frame.updated_at == arrow.get(3630) + assert frame.message is None def test_frames_with_empty_file(mock, watson): From 5993b2f17e589aadc26aedebc673dfc332586d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 8 Feb 2019 23:18:04 -0500 Subject: [PATCH 32/49] Fix test_stop_started_project_without_message --- tests/test_watson.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index 0e5276e8..036685a2 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -348,13 +348,14 @@ def test_stop_started_project_without_tags(watson): def test_stop_started_project_without_message(watson): + """Test stopping watson without adding a message.""" watson.start('foo') watson.stop() assert watson.current == {} assert watson.is_started is False assert len(watson.frames) == 1 - frame = watson.frames.get_by_index(0) + frame = watson.frames[0] assert frame.project == 'foo' assert frame.message is None From a76cbd3dc86085dad54e31d323261dc2bf04326d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 8 Feb 2019 23:18:14 -0500 Subject: [PATCH 33/49] Fix test_stop_started_project_with_message --- tests/test_watson.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index 036685a2..eecfbc4a 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -361,6 +361,7 @@ def test_stop_started_project_without_message(watson): def test_stop_started_project_with_message(watson): + """Test stopping watson when adding a message.""" watson.start('foo') watson._current['message'] = "My hovercraft is full of eels" watson.stop() @@ -368,7 +369,7 @@ def test_stop_started_project_with_message(watson): assert watson.current == {} assert watson.is_started is False assert len(watson.frames) == 1 - frame = watson.frames.get_by_index(0) + frame = watson.frames[0] assert frame.project == 'foo' assert frame.message == "My hovercraft is full of eels" From ed469a8e002475b8b9f8be033ee3448035d15d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 8 Feb 2019 23:22:33 -0500 Subject: [PATCH 34/49] Remove blank spaces --- tests/test_watson.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index eecfbc4a..1303f6a8 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -162,7 +162,7 @@ def test_frames_with_message(mocker, watson): [3601, 3610, 'foo', 'abcdefg', ['A', 'B', 'C'], 3650, "My hovercraft is full of eels"] ]) - + mocker.patch('%s.open' % builtins, mocker.mock_open(read_data=content)) assert len(watson.frames) == 1 frame = watson.frames['abcdefg'] From 61427d68b6e544418ad46654071425e4454d5918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 8 Feb 2019 23:32:46 -0500 Subject: [PATCH 35/49] Fix test_save_empty_current --- tests/test_watson.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index 1303f6a8..d13162a3 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -432,17 +432,18 @@ def test_save_current_without_tags(mock, watson, json_mock): assert dump_args['ensure_ascii'] is False -def test_save_empty_current(config_dir, mock, json_mock): +def test_save_empty_current(config_dir, mocker, json_mock): watson = Watson(current={}, config_dir=config_dir) - mock.patch('%s.open' % builtins, mock.mock_open()) + mocker.patch('%s.open' % builtins, mocker.mock_open()) watson.current = {'project': 'foo', 'start': 4000} watson.save() assert json_mock.call_count == 1 result = json_mock.call_args[0][0] - assert result == {'project': 'foo', 'start': 4000, 'tags': []} + assert result == {'project': 'foo', 'start': 4000, + 'tags': [], 'message': None} watson.current = {} watson.save() From 8ad05ada0c88409fd10d764ede8a257bda456b6a Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Sat, 16 Nov 2019 16:28:04 -0500 Subject: [PATCH 36/49] fix: flake8 error --- watson/watson.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/watson/watson.py b/watson/watson.py index f5c27605..b70d7913 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -291,9 +291,10 @@ def stop(self, stop_at=None): raise WatsonError('Task cannot end in the future.') frame = self.frames.add( - old['project'], old['start'], stop_at, tags=old['tags'], message=old.get('message') + old['project'], old['start'], stop_at, tags=old['tags'], + message=old.get('message') ) - + self.current = None return frame From bb17bbfe741fd169d3b177f4af071256df4d4c0d Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Sat, 16 Nov 2019 16:58:45 -0500 Subject: [PATCH 37/49] add venv to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 41ea6bdc..d02b1012 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ site/ .vagrant/ .venv/ .pytest_cache/ +venv/ # files From 4e051d1f51489b36ca9e847a02c9bcfa945739bf Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Sat, 16 Nov 2019 16:59:19 -0500 Subject: [PATCH 38/49] fix: message wasn't saving; move some logic from cli to watson --- watson/cli.py | 8 ++++---- watson/watson.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index 60e78bc1..fe4b599e 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -7,6 +7,8 @@ import os import re +from pprint import pprint + from dateutil import tz from functools import reduce, wraps @@ -270,10 +272,7 @@ def stop(watson, at_, message): $ watson stop --at 13:37 Stopping project apollo11, started an hour ago and stopped 30 minutes ago. (id: e9ccd52) # noqa: E501 """ - if watson.is_started and message is not None: - watson.current['message'] = message - - frame = watson.stop(stop_at=at_) + frame = watson.stop(stop_at=at_, message=message) output_str = u"Stopping project {}{}, started {} and stopped {}. (id: {})" click.echo(output_str.format( @@ -283,6 +282,7 @@ def stop(watson, at_, message): style('time', frame.stop.humanize()), style('short_id', frame.id), )) + if frame.message is not None: click.echo("Log message: {}".format(style('message', frame.message))) diff --git a/watson/watson.py b/watson/watson.py index b70d7913..0b14a319 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -142,6 +142,7 @@ def save(self): if not os.path.isdir(self._dir): os.makedirs(self._dir) + pprint(self._current) if self._current is not None and self._old_state != self._current: if self.is_started: current = { @@ -150,9 +151,11 @@ def save(self): 'tags': self.current['tags'], 'message': self.current.get('message'), } + pprint(current) else: current = {} - + pprint(current) + safe_save(self.state_file, make_json_writer(lambda: current)) self._old_state = current @@ -272,7 +275,7 @@ def start(self, project, tags=None, restart=False, gap=True): self.current = new_frame return self.current - def stop(self, stop_at=None): + def stop(self, stop_at=None, message=None): if not self.is_started: raise WatsonError("No project started.") @@ -290,9 +293,12 @@ def stop(self, stop_at=None): if stop_at > arrow.now(): raise WatsonError('Task cannot end in the future.') + if message is None: + message = old.get('message') + frame = self.frames.add( old['project'], old['start'], stop_at, tags=old['tags'], - message=old.get('message') + message=message ) self.current = None From 7038bb60a46c7d282c072432d49965ba19853ba6 Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Sat, 16 Nov 2019 17:31:00 -0500 Subject: [PATCH 39/49] write message to csv, json --- tests/test_utils.py | 4 ++-- watson/cli.py | 2 -- watson/utils.py | 2 ++ watson/watson.py | 5 +---- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 4b84b974..42d76b9a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -283,7 +283,7 @@ def test_frames_to_csv(watson): result = frames_to_csv(watson.frames) read_csv = list(csv.reader(StringIO(result))) - header = ['id', 'start', 'stop', 'project', 'tags'] + header = ['id', 'start', 'stop', 'project', 'tags', 'message'] assert len(read_csv) == 2 assert read_csv[0] == header assert read_csv[1][3] == 'foo' @@ -302,7 +302,7 @@ def test_frames_to_json(watson): result = json.loads(frames_to_json(watson.frames)) - keys = {'id', 'start', 'stop', 'project', 'tags'} + keys = {'id', 'start', 'stop', 'project', 'tags', 'message'} assert len(result) == 1 assert set(result[0].keys()) == keys assert result[0]['project'] == 'foo' diff --git a/watson/cli.py b/watson/cli.py index fe4b599e..f64c06d7 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -7,8 +7,6 @@ import os import re -from pprint import pprint - from dateutil import tz from functools import reduce, wraps diff --git a/watson/utils.py b/watson/utils.py index cb51c04b..b532d7f0 100644 --- a/watson/utils.py +++ b/watson/utils.py @@ -314,6 +314,7 @@ def frames_to_json(frames): ('stop', frame.stop.isoformat()), ('project', frame.project), ('tags', frame.tags), + ('message', frame.message), ]) for frame in frames ] @@ -336,6 +337,7 @@ def frames_to_csv(frames): ('stop', frame.stop.format('YYYY-MM-DD HH:mm:ss')), ('project', frame.project), ('tags', ', '.join(frame.tags)), + ('message', frame.message if frame.message else "") ]) for frame in frames ] diff --git a/watson/watson.py b/watson/watson.py index 0b14a319..f9286027 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -142,7 +142,6 @@ def save(self): if not os.path.isdir(self._dir): os.makedirs(self._dir) - pprint(self._current) if self._current is not None and self._old_state != self._current: if self.is_started: current = { @@ -151,11 +150,9 @@ def save(self): 'tags': self.current['tags'], 'message': self.current.get('message'), } - pprint(current) else: current = {} - pprint(current) - + safe_save(self.state_file, make_json_writer(lambda: current)) self._old_state = current From ecd4d30f2228583c9d85df581461b4ffda19729c Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Sat, 16 Nov 2019 20:49:58 -0500 Subject: [PATCH 40/49] update stop message test --- tests/test_watson.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index 81199abe..9aa9082b 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -372,8 +372,7 @@ def test_stop_started_project_without_message(watson): def test_stop_started_project_with_message(watson): """Test stopping watson when adding a message.""" watson.start('foo') - watson._current['message'] = "My hovercraft is full of eels" - watson.stop() + watson.stop(None, "My hovercraft is full of eels") assert watson.current == {} assert watson.is_started is False From 588c50ceb308207dd26c6f5464654870d5ab009a Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Tue, 10 Dec 2019 18:52:10 -0500 Subject: [PATCH 41/49] properly handle messages in edit; pass message to start --- tests/test_watson.py | 1 + watson/cli.py | 42 +++++++++++++++++++++++++++++++++--------- watson/utils.py | 8 ++++++++ watson/watson.py | 5 +++-- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index 3d8fb41e..6d460b74 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -197,6 +197,7 @@ def test_frames_without_message(mocker, watson): assert frame.updated_at == arrow.get(3630) assert frame.message is None + def test_frames_with_empty_file(mocker, watson): mocker.patch('%s.open' % builtins, mocker.mock_open(read_data="")) mocker.patch('os.path.getsize', return_value=0) diff --git a/watson/cli.py b/watson/cli.py index a070f06e..71a36871 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -28,6 +28,7 @@ confirm_project, confirm_tags, create_watson, + echo_frame_message, flatten_report_for_csv, format_timedelta, frames_to_csv, @@ -161,16 +162,22 @@ def help(ctx, command): click.echo(cmd.get_help(ctx)) -def _start(watson, project, tags, restart=False, gap=True): +def _start(watson, project, tags, restart=False, gap=True, + message=None): """ Start project with given list of tags and save status. """ - current = watson.start(project, tags, restart=restart, gap=gap) + current = watson.start(project, tags, restart=restart, gap=gap, + message=message) + # TODO: Update status to include message click.echo(u"Starting project {}{} at {}".format( style('project', project), (" " if current['tags'] else "") + style('tags', current['tags']), style('time', "{:HH:mm}".format(current['start'])) )) + + echo_frame_message(current) + watson.save() @@ -184,10 +191,13 @@ def _start(watson, project, tags, restart=False, gap=True): help="Confirm addition of new project.") @click.option('-b', '--confirm-new-tag', is_flag=True, default=False, help="Confirm creation of new tag.") +@click.option('-m', '--message', type=str, default=None, + help="A brief note that describe time entry being started") @click.pass_obj @click.pass_context @catch_watson_error -def start(ctx, watson, confirm_new_project, confirm_new_tag, args, gap_=True): +def start(ctx, watson, confirm_new_project, confirm_new_tag, args, gap_=True, + message=None): """ Start monitoring time for the given project. You can add tags indicating more specifically what you are working on with @@ -227,6 +237,7 @@ def start(ctx, watson, confirm_new_project, confirm_new_tag, args, gap_=True): if project and watson.is_started and not gap_: current = watson.current + # TODO: log in error message errmsg = ("Project '{}' is already started and '--no-gap' is passed. " "Please stop manually.") raise click.ClickException( @@ -239,7 +250,7 @@ def start(ctx, watson, confirm_new_project, confirm_new_tag, args, gap_=True): watson.config.getboolean('options', 'stop_on_start')): ctx.invoke(stop) - _start(watson, project, tags, gap=gap_) + _start(watson, project, tags, gap=gap_, message=message) @cli.command(context_settings={'ignore_unknown_options': True}) @@ -283,8 +294,11 @@ def stop(watson, at_, message): style('short_id', frame.id), )) - if frame.message is not None: - click.echo("Log message: {}".format(style('message', frame.message))) + if frame.message: + click.echo(u"{}{}".format( + style('message', '>> '), + style('message', frame.message) + )) watson.save() @@ -426,6 +440,12 @@ def status(watson, project, tags, elapsed): style('time', current['start'].strftime(timefmt)) )) + if current['message']: + click.echo(u"{}{}".format( + style('message', '>> '), + style('message', current['message']) + )) + _SHORTCUT_OPTIONS = ['all', 'year', 'month', 'luna', 'week', 'day'] _SHORTCUT_OPTIONS_VALUES = { @@ -1233,7 +1253,8 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): id = frame.id elif watson.is_started: frame = Frame(watson.current['start'], None, watson.current['project'], - None, watson.current['tags']) + None, watson.current['tags'], None, + watson.current['message']) elif watson.frames: frame = watson.frames[-1] id = frame.id @@ -1246,12 +1267,13 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): 'start': frame.start.format(datetime_format), 'project': frame.project, 'tags': frame.tags, + 'message': "" if frame.message is None else frame.message, } if id: data['stop'] = frame.stop.format(datetime_format) - if frame.message is not None: + if frame.message is not None and len(frame.message) > 0: data['message'] = frame.message text = json.dumps(data, indent=4, sort_keys=True, ensure_ascii=False) @@ -1284,12 +1306,12 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): tzinfo=local_tz).to('utc') stop = arrow.get(data['stop'], datetime_format).replace( tzinfo=local_tz).to('utc') if id else None - message = data.get('message') # if start time of the project is not before end time # raise ValueException if not watson.is_started and start > stop: raise ValueError( "Task cannot end before it starts.") + message = data.get('message') # break out of while loop and continue execution of # the edit function normally break @@ -1322,6 +1344,8 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): watson.current = dict(start=start, project=project, tags=tags, message=message) + print("message") + print(message) watson.save() click.echo( u"Edited frame for project {project}{tags}, from {start} to {stop} " diff --git a/watson/utils.py b/watson/utils.py index 280ef59d..518f0484 100644 --- a/watson/utils.py +++ b/watson/utils.py @@ -423,3 +423,11 @@ def json_arrow_encoder(obj): return obj.for_json() raise TypeError("Object {} is not JSON serializable".format(obj)) + + +def echo_frame_message(frame): + if frame['message']: + click.echo(u"{}{}".format( + style('message', '>> '), + style('message', frame['message']) + )) diff --git a/watson/watson.py b/watson/watson.py index f9286027..39d9d0b9 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -253,7 +253,7 @@ def add(self, project, from_date, to_date, tags): frame = self.frames.add(project, from_date, to_date, tags=tags) return frame - def start(self, project, tags=None, restart=False, gap=True): + def start(self, project, tags=None, restart=False, gap=True, message=None): if self.is_started: raise WatsonError( u"Project {} is already started.".format( @@ -265,7 +265,8 @@ def start(self, project, tags=None, restart=False, gap=True): if not restart: tags = (tags or []) + default_tags - new_frame = {'project': project, 'tags': deduplicate(tags)} + new_frame = {'project': project, 'tags': deduplicate(tags), + 'message': message} if not gap: stop_of_prev_frame = self.frames[-1].stop new_frame['start'] = stop_of_prev_frame From b288f584a1577dfe7e5a785cc2c95adba3fabc86 Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Wed, 11 Dec 2019 18:55:07 -0500 Subject: [PATCH 42/49] restructure log print loop --- watson/cli.py | 54 +++++++++++++++++++++++++++---------------------- watson/utils.py | 12 +++++++++-- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index 71a36871..0c17a0d7 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -28,7 +28,7 @@ confirm_project, confirm_tags, create_watson, - echo_frame_message, + echo_dict_message, flatten_report_for_csv, format_timedelta, frames_to_csv, @@ -176,7 +176,7 @@ def _start(watson, project, tags, restart=False, gap=True, style('time', "{:HH:mm}".format(current['start'])) )) - echo_frame_message(current) + echo_dict_message(current) watson.save() @@ -942,10 +942,12 @@ def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") +@click.option('-m/-M', '--messages/--no-messages', 'messages', default=True, + help="(Don't) output messages.") @click.pass_obj @catch_watson_error def log(watson, current, from_, to, projects, tags, year, month, week, day, - luna, all, output_format, pager): + luna, all, output_format, pager, messages): """ Display each recorded session during the given timespan. @@ -971,6 +973,9 @@ def log(watson, current, from_, to, projects, tags, year, month, week, day, `--json` option or to *CSV* using the `--csv` option. Only one of these two options can be used at once. + You can control whether or not messages for each frame are displayed by + passing --messages or --no-messages. + Example: \b @@ -1001,12 +1006,12 @@ def log(watson, current, from_, to, projects, tags, year, month, week, day, 1070ddb 13:48 to 16:17 2h 29m 11s voyager1 [antenna, sensors] \b $ watson log --from 2014-04-16 --to 2014-04-17 --csv - id,start,stop,project,tags - a96fcde,2014-04-17 09:15,2014-04-17 09:43,hubble,"lens, camera, transmission" - 5e91316,2014-04-17 10:19,2014-04-17 12:59,hubble,"camera, transmission" - 761dd51,2014-04-17 14:42,2014-04-17 15:54,voyager1,antenna - 02cb269,2014-04-16 09:53,2014-04-16 12:43,apollo11,wheels - 1070ddb,2014-04-16 13:48,2014-04-16 16:17,voyager1,"antenna, sensors" + id,start,stop,project,tags,message + a96fcde,2014-04-17 09:15,2014-04-17 09:43,hubble,"lens, camera, transmission", + 5e91316,2014-04-17 10:19,2014-04-17 12:59,hubble,"camera, transmission", + 761dd51,2014-04-17 14:42,2014-04-17 15:54,voyager1,antenna, + 02cb269,2014-04-16 09:53,2014-04-16 12:43,apollo11,wheels, + 1070ddb,2014-04-16 13:48,2014-04-16 16:17,voyager1,"antenna, sensors", """ # noqa for start_time in (_ for _ in [day, week, month, luna, year, all] if _ is not None): @@ -1020,7 +1025,8 @@ def log(watson, current, from_, to, projects, tags, year, month, week, day, watson.config.getboolean('options', 'log_current')): cur = watson.current watson.frames.add(cur['project'], cur['start'], arrow.utcnow(), - cur['tags'], id="current") + cur['tags'], id="current", + message=cur['message']) span = watson.frames.span(from_, to) filtered_frames = watson.frames.filter( @@ -1077,20 +1083,20 @@ def _final_print(lines): ) ) - _print("\n".join( - u"\t{id} {start} to {stop} {delta:>11} {project}{tags}".format( - delta=format_timedelta(frame.stop - frame.start), - project=style('project', u'{:>{}}'.format( - frame.project, longest_project - )), - pad=longest_project, - tags=(" "*2 if frame.tags else "") + style('tags', frame.tags), - start=style('time', '{:HH:mm}'.format(frame.start)), - stop=style('time', '{:HH:mm}'.format(frame.stop)), - id=style('short_id', frame.id) - ) - for frame in frames - )) + for frame in frames: + _print(u"\t{id} {start} to {stop} {delta:>11} {project}{tags}" + .format( + delta=format_timedelta(frame.stop - frame.start), + project=style('project', u'{:>{}}'.format( + frame.project, longest_project + )), + pad=longest_project, + tags=(" "*2 if frame.tags else "") + + style('tags', frame.tags), + start=style('time', '{:HH:mm}'.format(frame.start)), + stop=style('time', '{:HH:mm}'.format(frame.stop)), + id=style('short_id', frame.id) + )) _final_print(lines) diff --git a/watson/utils.py b/watson/utils.py index 518f0484..33c7f174 100644 --- a/watson/utils.py +++ b/watson/utils.py @@ -426,8 +426,16 @@ def json_arrow_encoder(obj): def echo_frame_message(frame): - if frame['message']: + echo_message(frame.message) + + +def echo_dict_message(dict): + echo_message(dict['message']) + + +def echo_message(message): + if message: click.echo(u"{}{}".format( style('message', '>> '), - style('message', frame['message']) + style('message', message) )) From b25bdca3cf2e9f8b81e0cf0e5e2d34a4960e259e Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Wed, 11 Dec 2019 19:24:38 -0500 Subject: [PATCH 43/49] refactor --- watson/cli.py | 22 ++++++++++------------ watson/utils.py | 20 +++++--------------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index 0c17a0d7..6eed4208 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -28,8 +28,8 @@ confirm_project, confirm_tags, create_watson, - echo_dict_message, flatten_report_for_csv, + format_message, format_timedelta, frames_to_csv, frames_to_json, @@ -169,14 +169,13 @@ def _start(watson, project, tags, restart=False, gap=True, """ current = watson.start(project, tags, restart=restart, gap=gap, message=message) - # TODO: Update status to include message click.echo(u"Starting project {}{} at {}".format( style('project', project), (" " if current['tags'] else "") + style('tags', current['tags']), style('time', "{:HH:mm}".format(current['start'])) )) - - echo_dict_message(current) + if message: + click.echo(format_message(message)) watson.save() @@ -278,7 +277,7 @@ def stop(watson, at_, message): $ watson stop -m "Done some thinking" Stopping project apollo11, started a minute ago. (id: e7ccd52) - Log message: Done some thinking + >> Done some thinking $ watson stop --at 13:37 Stopping project apollo11, started an hour ago and stopped 30 minutes ago. (id: e9ccd52) # noqa: E501 @@ -295,10 +294,7 @@ def stop(watson, at_, message): )) if frame.message: - click.echo(u"{}{}".format( - style('message', '>> '), - style('message', frame.message) - )) + click.echo(format_message(frame.message)) watson.save() @@ -942,12 +938,12 @@ def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") -@click.option('-m/-M', '--messages/--no-messages', 'messages', default=True, +@click.option('-m/-M', '--message/--no-message', 'show_messages', default=True, help="(Don't) output messages.") @click.pass_obj @catch_watson_error def log(watson, current, from_, to, projects, tags, year, month, week, day, - luna, all, output_format, pager, messages): + luna, all, output_format, pager, show_messages): """ Display each recorded session during the given timespan. @@ -1097,6 +1093,8 @@ def _final_print(lines): stop=style('time', '{:HH:mm}'.format(frame.stop)), id=style('short_id', frame.id) )) + if frame.message is not None and show_messages: + _print(u"\t{}{}".format(" "*9, format_message(frame.message))) _final_print(lines) @@ -1371,7 +1369,7 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): ) if message is not None: - click.echo("Log message: {}".format(style('message', message))) + click.echo("Message: {}".format(style('message', message))) @cli.command(context_settings={'ignore_unknown_options': True}) diff --git a/watson/utils.py b/watson/utils.py index 33c7f174..35b68300 100644 --- a/watson/utils.py +++ b/watson/utils.py @@ -424,18 +424,8 @@ def json_arrow_encoder(obj): raise TypeError("Object {} is not JSON serializable".format(obj)) - -def echo_frame_message(frame): - echo_message(frame.message) - - -def echo_dict_message(dict): - echo_message(dict['message']) - - -def echo_message(message): - if message: - click.echo(u"{}{}".format( - style('message', '>> '), - style('message', message) - )) +def format_message(message): + return u"{}{}".format( + style('message', '>> '), + style('message', message) + ) From 19f36fce6bfdb02eeab23041b96ad29767b0cd0d Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Wed, 11 Dec 2019 20:20:16 -0500 Subject: [PATCH 44/49] include messages when building reports --- watson/watson.py | 47 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/watson/watson.py b/watson/watson.py index 39d9d0b9..9dd52265 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -484,6 +484,9 @@ def report(self, from_, to, current=None, projects=None, tags=None, span = self.frames.span(from_, to) + if tags is None: + tags = [] + frames_by_project = sorted_groupby( self.frames.filter( projects=projects or None, tags=tags or None, @@ -516,20 +519,41 @@ def report(self, from_, to, current=None, projects=None, tags=None, ) total += delta - project_report = { - 'name': project, - 'time': delta.total_seconds(), - 'tags': [] - } - - if tags is None: - tags = [] - tags_to_print = sorted( set(tag for frame in frames for tag in frame.tags if tag in tags or not tags) ) + project_messages = [] + for frame in frames: + tags_match = len(set(frame.tags) & set(tags_to_print)) > 0 + if not frame.tags and frame.message: + # If this frame has no tags, and the user wants to print + # out all frames (they didn't specify a tag filter), add + # this message here. + project_messages.append(frame.message) + + # If the user is trying to print out all frames in the project + # (tags will be empty because no tags were passed) + if not tags: + # And this frame has no tags... + if not frame.tags: + # Add it to the project-level messages because it + # won't get included in the tag-level messages + # because it has no tag. + project_messages.append(frame.message) + # And this frame has a tag... + else: + # Let the tag-level filter handle this frame later on + pass + + project_report = { + 'name': project, + 'time': delta.total_seconds(), + 'tags': [], + 'messages': project_messages, + } + for tag in tags_to_print: delta = reduce( operator.add, @@ -537,9 +561,12 @@ def report(self, from_, to, current=None, projects=None, tags=None, datetime.timedelta() ) + tag_messages = [frame.message for frame in frames if tag in frame.tags and frame.message] + project_report['tags'].append({ 'name': tag, - 'time': delta.total_seconds() + 'time': delta.total_seconds(), + 'messages': tag_messages }) report['projects'].append(project_report) From 68d29187568a3d5053709e80c6618a26d1434ce7 Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Wed, 11 Dec 2019 20:21:44 -0500 Subject: [PATCH 45/49] clean up flake8 errors --- watson/utils.py | 1 + watson/watson.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/watson/utils.py b/watson/utils.py index 35b68300..15208506 100644 --- a/watson/utils.py +++ b/watson/utils.py @@ -424,6 +424,7 @@ def json_arrow_encoder(obj): raise TypeError("Object {} is not JSON serializable".format(obj)) + def format_message(message): return u"{}{}".format( style('message', '>> '), diff --git a/watson/watson.py b/watson/watson.py index 9dd52265..dd214ccb 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -526,7 +526,6 @@ def report(self, from_, to, current=None, projects=None, tags=None, project_messages = [] for frame in frames: - tags_match = len(set(frame.tags) & set(tags_to_print)) > 0 if not frame.tags and frame.message: # If this frame has no tags, and the user wants to print # out all frames (they didn't specify a tag filter), add @@ -561,7 +560,8 @@ def report(self, from_, to, current=None, projects=None, tags=None, datetime.timedelta() ) - tag_messages = [frame.message for frame in frames if tag in frame.tags and frame.message] + tag_messages = [frame.message for frame in frames + if tag in frame.tags and frame.message] project_report['tags'].append({ 'name': tag, From aeb56cc26ac3d2d41a746bf7e4a0706e093948c7 Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Wed, 11 Dec 2019 20:58:02 -0500 Subject: [PATCH 46/49] add tests for report log messages --- tests/test_watson.py | 34 ++++++++++++++++++++++++++++++++-- watson/watson.py | 6 ------ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index 6d460b74..a26a86ce 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -823,9 +823,12 @@ def test_report(watson): assert 'time' in report['projects'][0]['tags'][0] assert report['projects'][0]['tags'][1]['name'] == 'B' assert 'time' in report['projects'][0]['tags'][1] + assert len(report['projects'][0]['messages']) == 0 + assert len(report['projects'][0]['tags'][0]['messages']) == 0 + assert len(report['projects'][0]['tags'][1]['messages']) == 0 watson.start('bar', tags=['C']) - watson.stop() + watson.stop(message='bar message') report = watson.report(arrow.now(), arrow.now()) assert len(report['projects']) == 2 @@ -834,6 +837,13 @@ def test_report(watson): assert len(report['projects'][0]['tags']) == 1 assert report['projects'][0]['tags'][0]['name'] == 'C' + assert len(report['projects'][1]['messages']) == 0 + assert len(report['projects'][1]['tags'][0]['messages']) == 0 + assert len(report['projects'][1]['tags'][1]['messages']) == 0 + assert len(report['projects'][0]['messages']) == 0 + assert len(report['projects'][0]['tags'][0]['messages']) == 1 + assert report['projects'][0]['tags'][0]['messages'][0] == 'bar message' + report = watson.report( arrow.now(), arrow.now(), projects=['foo'], tags=['B'] ) @@ -843,16 +853,36 @@ def test_report(watson): assert report['projects'][0]['tags'][0]['name'] == 'B' watson.start('baz', tags=['D']) - watson.stop() + watson.stop(message='baz message') + + watson.start('foo') + watson.stop(message='foo no tags') + + watson.start('foo', tags=['A']) + watson.stop(message='foo one tag A') report = watson.report(arrow.now(), arrow.now(), projects=["foo"]) + assert len(report['projects']) == 1 + assert len(report['projects'][0]['messages']) == 1 + # A project-level message because this frame has no tags + assert report['projects'][0]['messages'][0] == 'foo no tags' + assert len(report['projects'][0]['tags']) == 2 + assert report['projects'][0]['tags'][0]['name'] == 'A' + assert report['projects'][0]['tags'][1]['name'] == 'B' + assert len(report['projects'][0]['tags'][0]['messages']) == 1 + assert len(report['projects'][0]['tags'][1]['messages']) == 0 + # A tag-level message because this frame has tags + assert report['projects'][0]['tags'][0]['messages'][0] == 'foo one tag A' report = watson.report(arrow.now(), arrow.now(), ignore_projects=["bar"]) assert len(report['projects']) == 2 report = watson.report(arrow.now(), arrow.now(), tags=["A"]) assert len(report['projects']) == 1 + assert len(report['projects'][0]['messages']) == 0 + assert len(report['projects'][0]['tags'][0]['messages']) == 1 + assert report['projects'][0]['tags'][0]['messages'][0] == 'foo one tag A' report = watson.report(arrow.now(), arrow.now(), ignore_tags=["D"]) assert len(report['projects']) == 2 diff --git a/watson/watson.py b/watson/watson.py index dd214ccb..1761d7de 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -526,12 +526,6 @@ def report(self, from_, to, current=None, projects=None, tags=None, project_messages = [] for frame in frames: - if not frame.tags and frame.message: - # If this frame has no tags, and the user wants to print - # out all frames (they didn't specify a tag filter), add - # this message here. - project_messages.append(frame.message) - # If the user is trying to print out all frames in the project # (tags will be empty because no tags were passed) if not tags: From 3a08968b5dbec2e6745a7b0d2b8c032b8d6538bb Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Wed, 11 Dec 2019 21:16:07 -0500 Subject: [PATCH 47/49] wip: report/aggregate messages --- watson/cli.py | 38 ++++++++++++++++++++++++++++++-------- watson/watson.py | 2 +- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index 6eed4208..5f887090 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -516,11 +516,13 @@ def status(watson, project, tags, elapsed): help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") +@click.option('-m', '--message', 'show_messages', default=False, + help="Show frame messages in report.") @click.pass_obj @catch_watson_error def report(watson, current, from_, to, projects, tags, ignore_projects, ignore_tags, year, month, week, day, luna, all, output_format, - pager, aggregated=False): + pager, aggregated=False, show_messages=False): """ Display a report of the time spent on each project. @@ -546,6 +548,10 @@ def report(watson, current, from_, to, projects, tags, ignore_projects, If you are outputting to the terminal, you can selectively enable a pager through the `--pager` option. + You can include frame messages in the report by passing the --messages + option. Messages will always be present in *JSON* reports. Messages are + never included in *CSV* reports. + You can change the output format for the report from *plain text* to *JSON* using the `--json` option or to *CSV* using the `--csv` option. Only one of these two options can be used at once. @@ -600,14 +606,16 @@ def report(watson, current, from_, to, projects, tags, ignore_projects, "tags": [ { "name": "export", - "time": 530.0 + "time": 530.0, + "messages": ["working hard"] }, { "name": "report", "time": 530.0 } ], - "time": 530.0 + "time": 530.0, + "messages": ["fixing bug #74", "refactor tests"] } ], "time": 530.0, @@ -706,6 +714,13 @@ def _final_print(lines): project=style('project', project['name']) )) + if show_messages: + for message in project['messages']: + _print(u'{tab}{message}'.format( + tab=tab, + message=format_message(message), + )) + tags = project['tags'] if tags: longest_tag = max(len(tag) for tag in tags or ['']) @@ -719,6 +734,13 @@ def _final_print(lines): tag['name'], longest_tag )), )) + + if show_messages: + for message in tag['messages']: + _print(u'\t{tab}{message}'.format( + tab=tab, + message=format_message(message), + )) _print("") # only show total time at the bottom for a project if it is not @@ -773,11 +795,13 @@ def _final_print(lines): help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") +@click.option('-m', '--message', 'show_messages', default=False, + help="Show frame messages in report.") @click.pass_obj @click.pass_context @catch_watson_error def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, - pager): + pager, show_messages): """ Display a report of the time spent on each project aggregated by day. @@ -856,7 +880,7 @@ def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, output = ctx.invoke(report, current=current, from_=from_offset, to=from_offset, projects=projects, tags=tags, output_format=output_format, - pager=pager, aggregated=True) + pager=pager, aggregated=True, show_messages=show_messages) if 'json' in output_format: lines.append(output) @@ -938,7 +962,7 @@ def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") -@click.option('-m/-M', '--message/--no-message', 'show_messages', default=True, +@click.option('-m/-M', '--messages/--no-messages', 'show_messages', default=True, help="(Don't) output messages.") @click.pass_obj @catch_watson_error @@ -1348,8 +1372,6 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): watson.current = dict(start=start, project=project, tags=tags, message=message) - print("message") - print(message) watson.save() click.echo( u"Edited frame for project {project}{tags}, from {start} to {stop} " diff --git a/watson/watson.py b/watson/watson.py index 1761d7de..b3f1fba2 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -528,7 +528,7 @@ def report(self, from_, to, current=None, projects=None, tags=None, for frame in frames: # If the user is trying to print out all frames in the project # (tags will be empty because no tags were passed) - if not tags: + if not tags and frame.message: # And this frame has no tags... if not frame.tags: # Add it to the project-level messages because it From d80f4db7f6f6bb7f309a79120ddab41659897fbc Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Sun, 12 Jan 2020 19:11:25 -0500 Subject: [PATCH 48/49] change -m/--message to -n/--note -m and -M are currently used by some commands. -n and -N aren't. This diverges from the git -m/--message naming we were copying, but that doesn't bother me too much. The parameter behaves slightly different in watson, so it might even be better that we don't reuse a commonly used parameter name in another tool, as not to cause confusion --- docs/user-guide/commands.md | 6 +- tests/test_utils.py | 4 +- tests/test_watson.py | 76 +++++++++++----------- watson/cli.py | 122 ++++++++++++++++++------------------ watson/frames.py | 12 ++-- watson/utils.py | 12 ++-- watson/watson.py | 34 +++++----- 7 files changed, 133 insertions(+), 133 deletions(-) diff --git a/docs/user-guide/commands.md b/docs/user-guide/commands.md index c1f08c90..58bf3dbd 100644 --- a/docs/user-guide/commands.md +++ b/docs/user-guide/commands.md @@ -690,13 +690,13 @@ specified time must be after the begin of the to be ended frame and must not be in the future. You can optionally pass a log message to be saved with the frame via -the ``-m/--message`` option. +the ``-n/--note`` option. Example: $ watson stop --at 13:37 Stopping project apollo11, started an hour ago and stopped 30 minutes ago. (id: e9ccd52) # noqa: E501 - $ watson stop -m "Done some thinking" + $ watson stop -n "Done some thinking" Stopping project apollo11, started a minute ago. (id: e7ccd52) Log message: Done some thinking @@ -705,7 +705,7 @@ Example: Flag | Help -----|----- `--at TIME` | Stop frame at this time. Must be in (YYYY-MM-DDT)?HH:MM(:SS)? format. -`-m, --message TEXT` | Save given log message with the project frame. +`-n, --note TEXT` | Save given log message with the project frame. `--help` | Show this message and exit. ## `sync` diff --git a/tests/test_utils.py b/tests/test_utils.py index 0ad4cdb9..08b1cc52 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -283,7 +283,7 @@ def test_frames_to_csv(watson): result = frames_to_csv(watson.frames) read_csv = list(csv.reader(StringIO(result))) - header = ['id', 'start', 'stop', 'project', 'tags', 'message'] + header = ['id', 'start', 'stop', 'project', 'tags', 'note'] assert len(read_csv) == 2 assert read_csv[0] == header assert read_csv[1][3] == 'foo' @@ -302,7 +302,7 @@ def test_frames_to_json(watson): result = json.loads(frames_to_json(watson.frames)) - keys = {'id', 'start', 'stop', 'project', 'tags', 'message'} + keys = {'id', 'start', 'stop', 'project', 'tags', 'note'} assert len(result) == 1 assert set(result[0].keys()) == keys assert result[0]['project'] == 'foo' diff --git a/tests/test_watson.py b/tests/test_watson.py index a26a86ce..0e2fcb5f 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -150,8 +150,8 @@ def test_frames_without_tags(mocker, watson): assert watson.frames[0].tags == [] -def test_frames_with_message(mocker, watson): - """Test loading frames with messages.""" +def test_frames_with_note(mocker, watson): + """Test loading frames with notes.""" content = json.dumps([ [3601, 3610, 'foo', 'abcdefg', ['A', 'B', 'C'], 3650, "My hovercraft is full of eels"] @@ -165,11 +165,11 @@ def test_frames_with_message(mocker, watson): assert frame.start == arrow.get(3601) assert frame.stop == arrow.get(3610) assert frame.tags == ['A', 'B', 'C'] - assert frame.message == "My hovercraft is full of eels" + assert frame.note == "My hovercraft is full of eels" -def test_frames_without_message(mocker, watson): - """Test loading frames without messages.""" +def test_frames_without_note(mocker, watson): + """Test loading frames without notes.""" content = json.dumps([ [3601, 3610, 'foo', 'abcdefg'], [3611, 3620, 'foo', 'hijklmn', ['A', 'B', 'C']], @@ -184,18 +184,18 @@ def test_frames_without_message(mocker, watson): assert frame.start == arrow.get(3601) assert frame.stop == arrow.get(3610) assert frame.tags == [] - assert frame.message is None + assert frame.note is None frame = watson.frames['hijklmn'] assert frame.id == 'hijklmn' assert frame.tags == ['A', 'B', 'C'] - assert frame.message is None + assert frame.note is None frame = watson.frames['opqrstu'] assert frame.id == 'opqrstu' assert frame.tags == ['A', 'B', 'C'] assert frame.updated_at == arrow.get(3630) - assert frame.message is None + assert frame.note is None def test_frames_with_empty_file(mocker, watson): @@ -350,8 +350,8 @@ def test_stop_started_project_without_tags(watson): assert watson.frames[0].tags == [] -def test_stop_started_project_without_message(watson): - """Test stopping watson without adding a message.""" +def test_stop_started_project_without_note(watson): + """Test stopping watson without adding a note.""" watson.start('foo') watson.stop() @@ -360,11 +360,11 @@ def test_stop_started_project_without_message(watson): assert len(watson.frames) == 1 frame = watson.frames[0] assert frame.project == 'foo' - assert frame.message is None + assert frame.note is None -def test_stop_started_project_with_message(watson): - """Test stopping watson when adding a message.""" +def test_stop_started_project_with_note(watson): + """Test stopping watson when adding a note.""" watson.start('foo') watson.stop(None, "My hovercraft is full of eels") @@ -373,7 +373,7 @@ def test_stop_started_project_with_message(watson): assert len(watson.frames) == 1 frame = watson.frames[0] assert frame.project == 'foo' - assert frame.message == "My hovercraft is full of eels" + assert frame.note == "My hovercraft is full of eels" def test_stop_no_project(watson): @@ -463,7 +463,7 @@ def test_save_empty_current(config_dir, mocker, json_mock): assert json_mock.call_count == 1 result = json_mock.call_args[0][0] assert result == {'project': 'foo', 'start': 4000, - 'tags': [], 'message': None} + 'tags': [], 'note': None} watson.current = {} watson.save() @@ -823,12 +823,12 @@ def test_report(watson): assert 'time' in report['projects'][0]['tags'][0] assert report['projects'][0]['tags'][1]['name'] == 'B' assert 'time' in report['projects'][0]['tags'][1] - assert len(report['projects'][0]['messages']) == 0 - assert len(report['projects'][0]['tags'][0]['messages']) == 0 - assert len(report['projects'][0]['tags'][1]['messages']) == 0 + assert len(report['projects'][0]['notes']) == 0 + assert len(report['projects'][0]['tags'][0]['notes']) == 0 + assert len(report['projects'][0]['tags'][1]['notes']) == 0 watson.start('bar', tags=['C']) - watson.stop(message='bar message') + watson.stop(note='bar note') report = watson.report(arrow.now(), arrow.now()) assert len(report['projects']) == 2 @@ -837,12 +837,12 @@ def test_report(watson): assert len(report['projects'][0]['tags']) == 1 assert report['projects'][0]['tags'][0]['name'] == 'C' - assert len(report['projects'][1]['messages']) == 0 - assert len(report['projects'][1]['tags'][0]['messages']) == 0 - assert len(report['projects'][1]['tags'][1]['messages']) == 0 - assert len(report['projects'][0]['messages']) == 0 - assert len(report['projects'][0]['tags'][0]['messages']) == 1 - assert report['projects'][0]['tags'][0]['messages'][0] == 'bar message' + assert len(report['projects'][1]['notes']) == 0 + assert len(report['projects'][1]['tags'][0]['notes']) == 0 + assert len(report['projects'][1]['tags'][1]['notes']) == 0 + assert len(report['projects'][0]['notes']) == 0 + assert len(report['projects'][0]['tags'][0]['notes']) == 1 + assert report['projects'][0]['tags'][0]['notes'][0] == 'bar note' report = watson.report( arrow.now(), arrow.now(), projects=['foo'], tags=['B'] @@ -853,36 +853,36 @@ def test_report(watson): assert report['projects'][0]['tags'][0]['name'] == 'B' watson.start('baz', tags=['D']) - watson.stop(message='baz message') + watson.stop(note='baz note') watson.start('foo') - watson.stop(message='foo no tags') + watson.stop(note='foo no tags') watson.start('foo', tags=['A']) - watson.stop(message='foo one tag A') + watson.stop(note='foo one tag A') report = watson.report(arrow.now(), arrow.now(), projects=["foo"]) assert len(report['projects']) == 1 - assert len(report['projects'][0]['messages']) == 1 - # A project-level message because this frame has no tags - assert report['projects'][0]['messages'][0] == 'foo no tags' + assert len(report['projects'][0]['notes']) == 1 + # A project-level note because this frame has no tags + assert report['projects'][0]['notes'][0] == 'foo no tags' assert len(report['projects'][0]['tags']) == 2 assert report['projects'][0]['tags'][0]['name'] == 'A' assert report['projects'][0]['tags'][1]['name'] == 'B' - assert len(report['projects'][0]['tags'][0]['messages']) == 1 - assert len(report['projects'][0]['tags'][1]['messages']) == 0 - # A tag-level message because this frame has tags - assert report['projects'][0]['tags'][0]['messages'][0] == 'foo one tag A' + assert len(report['projects'][0]['tags'][0]['notes']) == 1 + assert len(report['projects'][0]['tags'][1]['notes']) == 0 + # A tag-level note because this frame has tags + assert report['projects'][0]['tags'][0]['notes'][0] == 'foo one tag A' report = watson.report(arrow.now(), arrow.now(), ignore_projects=["bar"]) assert len(report['projects']) == 2 report = watson.report(arrow.now(), arrow.now(), tags=["A"]) assert len(report['projects']) == 1 - assert len(report['projects'][0]['messages']) == 0 - assert len(report['projects'][0]['tags'][0]['messages']) == 1 - assert report['projects'][0]['tags'][0]['messages'][0] == 'foo one tag A' + assert len(report['projects'][0]['notes']) == 0 + assert len(report['projects'][0]['tags'][0]['notes']) == 1 + assert report['projects'][0]['tags'][0]['notes'][0] == 'foo one tag A' report = watson.report(arrow.now(), arrow.now(), ignore_tags=["D"]) assert len(report['projects']) == 2 diff --git a/watson/cli.py b/watson/cli.py index 5f887090..c989e449 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -29,7 +29,7 @@ confirm_tags, create_watson, flatten_report_for_csv, - format_message, + format_note, format_timedelta, frames_to_csv, frames_to_json, @@ -163,19 +163,19 @@ def help(ctx, command): def _start(watson, project, tags, restart=False, gap=True, - message=None): + note=None): """ Start project with given list of tags and save status. """ current = watson.start(project, tags, restart=restart, gap=gap, - message=message) + note=note) click.echo(u"Starting project {}{} at {}".format( style('project', project), (" " if current['tags'] else "") + style('tags', current['tags']), style('time', "{:HH:mm}".format(current['start'])) )) - if message: - click.echo(format_message(message)) + if note: + click.echo(format_note(note)) watson.save() @@ -190,13 +190,13 @@ def _start(watson, project, tags, restart=False, gap=True, help="Confirm addition of new project.") @click.option('-b', '--confirm-new-tag', is_flag=True, default=False, help="Confirm creation of new tag.") -@click.option('-m', '--message', type=str, default=None, +@click.option('-n', '--note', type=str, default=None, help="A brief note that describe time entry being started") @click.pass_obj @click.pass_context @catch_watson_error def start(ctx, watson, confirm_new_project, confirm_new_tag, args, gap_=True, - message=None): + note=None): """ Start monitoring time for the given project. You can add tags indicating more specifically what you are working on with @@ -236,7 +236,7 @@ def start(ctx, watson, confirm_new_project, confirm_new_tag, args, gap_=True, if project and watson.is_started and not gap_: current = watson.current - # TODO: log in error message + # TODO: log in error note errmsg = ("Project '{}' is already started and '--no-gap' is passed. " "Please stop manually.") raise click.ClickException( @@ -249,18 +249,18 @@ def start(ctx, watson, confirm_new_project, confirm_new_tag, args, gap_=True, watson.config.getboolean('options', 'stop_on_start')): ctx.invoke(stop) - _start(watson, project, tags, gap=gap_, message=message) + _start(watson, project, tags, gap=gap_, note=note) @cli.command(context_settings={'ignore_unknown_options': True}) @click.option('--at', 'at_', type=DateTime, default=None, help=('Stop frame at this time. Must be in ' '(YYYY-MM-DDT)?HH:MM(:SS)? format.')) -@click.option('-m', '--message', 'message', default=None, - help="Save given log message with the project frame.") +@click.option('-n', '--note', 'note', default=None, + help="Save given log note with the project frame.") @click.pass_obj @catch_watson_error -def stop(watson, at_, message): +def stop(watson, at_, note): """ Stop monitoring time for the current project. @@ -269,20 +269,20 @@ def stop(watson, at_, message): not be in the future. You can optionally pass a log message to be saved with the frame via - the ``-m/--message`` option. + the ``-n/--note`` option. Examples: \b - $ watson stop -m "Done some thinking" + $ watson stop -n "Done some thinking" Stopping project apollo11, started a minute ago. (id: e7ccd52) >> Done some thinking $ watson stop --at 13:37 Stopping project apollo11, started an hour ago and stopped 30 minutes ago. (id: e9ccd52) # noqa: E501 """ - frame = watson.stop(stop_at=at_, message=message) + frame = watson.stop(stop_at=at_, note=note) output_str = u"Stopping project {}{}, started {} and stopped {}. (id: {})" click.echo(output_str.format( @@ -293,8 +293,8 @@ def stop(watson, at_, message): style('short_id', frame.id), )) - if frame.message: - click.echo(format_message(frame.message)) + if frame.note: + click.echo(format_note(frame.note)) watson.save() @@ -436,10 +436,10 @@ def status(watson, project, tags, elapsed): style('time', current['start'].strftime(timefmt)) )) - if current['message']: + if current['note']: click.echo(u"{}{}".format( - style('message', '>> '), - style('message', current['message']) + style('note', '>> '), + style('note', current['note']) )) @@ -516,13 +516,13 @@ def status(watson, project, tags, elapsed): help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") -@click.option('-m', '--message', 'show_messages', default=False, - help="Show frame messages in report.") +@click.option('-n', '--note', 'show_notes', default=False, + help="Show frame notes in report.") @click.pass_obj @catch_watson_error def report(watson, current, from_, to, projects, tags, ignore_projects, ignore_tags, year, month, week, day, luna, all, output_format, - pager, aggregated=False, show_messages=False): + pager, aggregated=False, show_notes=False): """ Display a report of the time spent on each project. @@ -548,7 +548,7 @@ def report(watson, current, from_, to, projects, tags, ignore_projects, If you are outputting to the terminal, you can selectively enable a pager through the `--pager` option. - You can include frame messages in the report by passing the --messages + You can include frame notes in the report by passing the --notes option. Messages will always be present in *JSON* reports. Messages are never included in *CSV* reports. @@ -607,7 +607,7 @@ def report(watson, current, from_, to, projects, tags, ignore_projects, { "name": "export", "time": 530.0, - "messages": ["working hard"] + "notes": ["working hard"] }, { "name": "report", @@ -615,7 +615,7 @@ def report(watson, current, from_, to, projects, tags, ignore_projects, } ], "time": 530.0, - "messages": ["fixing bug #74", "refactor tests"] + "notes": ["fixing bug #74", "refactor tests"] } ], "time": 530.0, @@ -714,11 +714,11 @@ def _final_print(lines): project=style('project', project['name']) )) - if show_messages: - for message in project['messages']: - _print(u'{tab}{message}'.format( + if show_notes: + for note in project['notes']: + _print(u'{tab}{note}'.format( tab=tab, - message=format_message(message), + note=format_note(note), )) tags = project['tags'] @@ -735,11 +735,11 @@ def _final_print(lines): )), )) - if show_messages: - for message in tag['messages']: - _print(u'\t{tab}{message}'.format( + if show_notes: + for note in tag['notes']: + _print(u'\t{tab}{note}'.format( tab=tab, - message=format_message(message), + note=format_note(note), )) _print("") @@ -795,13 +795,13 @@ def _final_print(lines): help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") -@click.option('-m', '--message', 'show_messages', default=False, - help="Show frame messages in report.") +@click.option('-n', '--note', 'show_notes', default=False, + help="Show frame notes in report.") @click.pass_obj @click.pass_context @catch_watson_error def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, - pager, show_messages): + pager, show_notes): """ Display a report of the time spent on each project aggregated by day. @@ -880,7 +880,7 @@ def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, output = ctx.invoke(report, current=current, from_=from_offset, to=from_offset, projects=projects, tags=tags, output_format=output_format, - pager=pager, aggregated=True, show_messages=show_messages) + pager=pager, aggregated=True, show_notes=show_notes) if 'json' in output_format: lines.append(output) @@ -962,12 +962,12 @@ def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") -@click.option('-m/-M', '--messages/--no-messages', 'show_messages', default=True, - help="(Don't) output messages.") +@click.option('-n/-N', '--notes/--no-notes', 'show_notes', default=True, + help="(Don't) output notes.") @click.pass_obj @catch_watson_error def log(watson, current, from_, to, projects, tags, year, month, week, day, - luna, all, output_format, pager, show_messages): + luna, all, output_format, pager, show_notes): """ Display each recorded session during the given timespan. @@ -993,8 +993,8 @@ def log(watson, current, from_, to, projects, tags, year, month, week, day, `--json` option or to *CSV* using the `--csv` option. Only one of these two options can be used at once. - You can control whether or not messages for each frame are displayed by - passing --messages or --no-messages. + You can control whether or not notes for each frame are displayed by + passing --notes or --no-notes. Example: @@ -1026,7 +1026,7 @@ def log(watson, current, from_, to, projects, tags, year, month, week, day, 1070ddb 13:48 to 16:17 2h 29m 11s voyager1 [antenna, sensors] \b $ watson log --from 2014-04-16 --to 2014-04-17 --csv - id,start,stop,project,tags,message + id,start,stop,project,tags,note a96fcde,2014-04-17 09:15,2014-04-17 09:43,hubble,"lens, camera, transmission", 5e91316,2014-04-17 10:19,2014-04-17 12:59,hubble,"camera, transmission", 761dd51,2014-04-17 14:42,2014-04-17 15:54,voyager1,antenna, @@ -1046,7 +1046,7 @@ def log(watson, current, from_, to, projects, tags, year, month, week, day, cur = watson.current watson.frames.add(cur['project'], cur['start'], arrow.utcnow(), cur['tags'], id="current", - message=cur['message']) + note=cur['note']) span = watson.frames.span(from_, to) filtered_frames = watson.frames.filter( @@ -1117,8 +1117,8 @@ def _final_print(lines): stop=style('time', '{:HH:mm}'.format(frame.stop)), id=style('short_id', frame.id) )) - if frame.message is not None and show_messages: - _print(u"\t{}{}".format(" "*9, format_message(frame.message))) + if frame.note is not None and show_notes: + _print(u"\t{}{}".format(" "*9, format_note(frame.note))) _final_print(lines) @@ -1282,7 +1282,7 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): elif watson.is_started: frame = Frame(watson.current['start'], None, watson.current['project'], None, watson.current['tags'], None, - watson.current['message']) + watson.current['note']) elif watson.frames: frame = watson.frames[-1] id = frame.id @@ -1295,14 +1295,14 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): 'start': frame.start.format(datetime_format), 'project': frame.project, 'tags': frame.tags, - 'message': "" if frame.message is None else frame.message, + 'note': "" if frame.note is None else frame.note, } if id: data['stop'] = frame.stop.format(datetime_format) - if frame.message is not None and len(frame.message) > 0: - data['message'] = frame.message + if frame.note is not None and len(frame.note) > 0: + data['note'] = frame.note text = json.dumps(data, indent=4, sort_keys=True, ensure_ascii=False) @@ -1339,7 +1339,7 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): if not watson.is_started and start > stop: raise ValueError( "Task cannot end before it starts.") - message = data.get('message') + note = data.get('note') # break out of while loop and continue execution of # the edit function normally break @@ -1362,15 +1362,15 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): if id: if all((project == frame.project, start == frame.start, stop == frame.stop, tags == frame.tags, - message == frame.message)): + note == frame.note)): updated_at = frame.updated_at else: updated_at = arrow.utcnow() - watson.frames[id] = (project, start, stop, tags, updated_at, message) + watson.frames[id] = (project, start, stop, tags, updated_at, note) else: watson.current = dict(start=start, project=project, tags=tags, - message=message) + note=note) watson.save() click.echo( @@ -1390,8 +1390,8 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): ) ) - if message is not None: - click.echo("Message: {}".format(style('message', message))) + if note is not None: + click.echo("Message: {}".format(style('note', note))) @cli.command(context_settings={'ignore_unknown_options': True}) @@ -1633,7 +1633,7 @@ def merge(watson, frames_with_conflict, force): 'start': original_frame.start.format(date_format), 'stop': original_frame.stop.format(date_format), 'tags': original_frame.tags, - 'message': original_frame.message + 'note': original_frame.note } click.echo("frame {}:".format(style('short_id', original_frame.id))) click.echo(u"{}".format('\n'.join('<' + line for line in json.dumps( @@ -1666,7 +1666,7 @@ def merge(watson, frames_with_conflict, force): 'start': conflict_frame_copy.start.format(date_format), 'stop': conflict_frame_copy.stop.format(date_format), 'tags': conflict_frame_copy.tags, - 'message': conflict_frame_copy.message + 'note': conflict_frame_copy.note } click.echo("{}".format('\n'.join('>' + line for line in json.dumps( conflict_frame_data, indent=4, ensure_ascii=False).splitlines()))) @@ -1680,9 +1680,9 @@ def merge(watson, frames_with_conflict, force): # merge in any non-conflicting frames for frame in merging: - start, stop, project, id, tags, updated_at, message = frame.dump() + start, stop, project, id, tags, updated_at, note = frame.dump() original_frames.add(project, start, stop, tags=tags, id=id, - updated_at=updated_at, message=message) + updated_at=updated_at, note=note) watson.frames = original_frames watson.frames.changed = True watson.save() diff --git a/watson/frames.py b/watson/frames.py index edb7a002..eeea903f 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -4,12 +4,12 @@ from collections import namedtuple -HEADERS = ('start', 'stop', 'project', 'id', 'tags', 'updated_at', 'message') +HEADERS = ('start', 'stop', 'project', 'id', 'tags', 'updated_at', 'note') class Frame(namedtuple('Frame', HEADERS)): def __new__(cls, start, stop, project, id, tags=None, updated_at=None, - message=None): + note=None): try: if not isinstance(start, arrow.Arrow): start = arrow.get(start) @@ -32,7 +32,7 @@ def __new__(cls, start, stop, project, id, tags=None, updated_at=None, tags = [] return super(Frame, cls).__new__( - cls, start, stop, project, id, tags, updated_at, message + cls, start, stop, project, id, tags, updated_at, note ) def dump(self): @@ -41,7 +41,7 @@ def dump(self): updated_at = self.updated_at.timestamp return (start, stop, self.project, self.id, self.tags, updated_at, - self.message) + self.note) @property def day(self): @@ -136,11 +136,11 @@ def add(self, *args, **kwargs): return frame def new_frame(self, project, start, stop, tags=None, id=None, - updated_at=None, message=None): + updated_at=None, note=None): if not id: id = uuid.uuid4().hex return Frame(start, stop, project, id, tags=tags, - updated_at=updated_at, message=message) + updated_at=updated_at, note=note) def dump(self): return tuple(frame.dump() for frame in self._rows) diff --git a/watson/utils.py b/watson/utils.py index 15208506..2c1a08dc 100644 --- a/watson/utils.py +++ b/watson/utils.py @@ -85,7 +85,7 @@ def _style_short_id(id): 'date': {'fg': 'cyan'}, 'short_id': _style_short_id, 'id': {'fg': 'white'}, - 'message': {'fg': 'white'}, + 'note': {'fg': 'white'}, } fmt = formats.get(name, {}) @@ -319,7 +319,7 @@ def frames_to_json(frames): ('stop', frame.stop.isoformat()), ('project', frame.project), ('tags', frame.tags), - ('message', frame.message), + ('note', frame.note), ]) for frame in frames ] @@ -342,7 +342,7 @@ def frames_to_csv(frames): ('stop', frame.stop.format('YYYY-MM-DD HH:mm:ss')), ('project', frame.project), ('tags', ', '.join(frame.tags)), - ('message', frame.message if frame.message else "") + ('note', frame.note if frame.note else "") ]) for frame in frames ] @@ -425,8 +425,8 @@ def json_arrow_encoder(obj): raise TypeError("Object {} is not JSON serializable".format(obj)) -def format_message(message): +def format_note(note): return u"{}{}".format( - style('message', '>> '), - style('message', message) + style('note', '>> '), + style('note', note) ) diff --git a/watson/watson.py b/watson/watson.py index b3f1fba2..69c03bae 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -148,7 +148,7 @@ def save(self): 'project': self.current['project'], 'start': self._format_date(self.current['start']), 'tags': self.current['tags'], - 'message': self.current.get('message'), + 'note': self.current.get('note'), } else: current = {} @@ -211,7 +211,7 @@ def current(self, value): 'project': value['project'], 'start': start, 'tags': value.get('tags') or [], - 'message': value.get('message'), + 'note': value.get('note'), } if self._old_state is None: @@ -253,7 +253,7 @@ def add(self, project, from_date, to_date, tags): frame = self.frames.add(project, from_date, to_date, tags=tags) return frame - def start(self, project, tags=None, restart=False, gap=True, message=None): + def start(self, project, tags=None, restart=False, gap=True, note=None): if self.is_started: raise WatsonError( u"Project {} is already started.".format( @@ -266,14 +266,14 @@ def start(self, project, tags=None, restart=False, gap=True, message=None): tags = (tags or []) + default_tags new_frame = {'project': project, 'tags': deduplicate(tags), - 'message': message} + 'note': note} if not gap: stop_of_prev_frame = self.frames[-1].stop new_frame['start'] = stop_of_prev_frame self.current = new_frame return self.current - def stop(self, stop_at=None, message=None): + def stop(self, stop_at=None, note=None): if not self.is_started: raise WatsonError("No project started.") @@ -291,12 +291,12 @@ def stop(self, stop_at=None, message=None): if stop_at > arrow.now(): raise WatsonError('Task cannot end in the future.') - if message is None: - message = old.get('message') + if note is None: + note = old.get('note') frame = self.frames.add( old['project'], old['start'], stop_at, tags=old['tags'], - message=message + note=note ) self.current = None @@ -524,17 +524,17 @@ def report(self, from_, to, current=None, projects=None, tags=None, if tag in tags or not tags) ) - project_messages = [] + project_notes = [] for frame in frames: # If the user is trying to print out all frames in the project # (tags will be empty because no tags were passed) - if not tags and frame.message: + if not tags and frame.note: # And this frame has no tags... if not frame.tags: - # Add it to the project-level messages because it - # won't get included in the tag-level messages + # Add it to the project-level notes because it + # won't get included in the tag-level notes # because it has no tag. - project_messages.append(frame.message) + project_notes.append(frame.note) # And this frame has a tag... else: # Let the tag-level filter handle this frame later on @@ -544,7 +544,7 @@ def report(self, from_, to, current=None, projects=None, tags=None, 'name': project, 'time': delta.total_seconds(), 'tags': [], - 'messages': project_messages, + 'notes': project_notes, } for tag in tags_to_print: @@ -554,13 +554,13 @@ def report(self, from_, to, current=None, projects=None, tags=None, datetime.timedelta() ) - tag_messages = [frame.message for frame in frames - if tag in frame.tags and frame.message] + tag_notes = [frame.note for frame in frames + if tag in frame.tags and frame.note] project_report['tags'].append({ 'name': tag, 'time': delta.total_seconds(), - 'messages': tag_messages + 'notes': tag_notes }) report['projects'].append(project_report) From c8e5edd7c73ad3907f768511063e455ea8e374cb Mon Sep 17 00:00:00 2001 From: prat0088 Date: Mon, 13 Jan 2020 09:28:16 -0500 Subject: [PATCH 49/49] fix: aggregate/report -n params not detected as flags on Windows --- watson/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index c989e449..ff903789 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -516,7 +516,7 @@ def status(watson, project, tags, elapsed): help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") -@click.option('-n', '--note', 'show_notes', default=False, +@click.option('-n', '--note', 'show_notes', default=False, is_flag=True, help="Show frame notes in report.") @click.pass_obj @catch_watson_error @@ -795,7 +795,7 @@ def _final_print(lines): help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") -@click.option('-n', '--note', 'show_notes', default=False, +@click.option('-n', '--note', 'show_notes', default=False, is_flag=True, help="Show frame notes in report.") @click.pass_obj @click.pass_context