Skip to content

Commit

Permalink
Allow specification of start time via start_at (#364)
Browse files Browse the repository at this point in the history
* Allow specification of start time via `start_at`
* Rename previous occurrences of "start_at" to "begin_at". Just to avoid confusion down the road.
* Indicate that "at" option also works with `restart`
  • Loading branch information
joelostblom authored Jun 18, 2020
1 parent ac8a23c commit 2cb871e
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Log output order can now be controlled via the `--reverse/--no-reverse` flag
and the `reverse_log` configuration option (#369)
- Add `--at` flag to the `start` and `restart` commands (#364).

### Changed

Expand Down
12 changes: 12 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,15 @@ def test_stop_valid_time(runner, watson, mocker, at_dt):
arrow.arrow.datetime.now.return_value = (start_dt + timedelta(hours=1))
result = runner.invoke(cli.stop, ['--at', at_dt], obj=watson)
assert result.exit_code == 0


# watson start

@pytest.mark.parametrize('at_dt', VALID_TIMES_DATA)
def test_start_valid_time(runner, watson, mocker, at_dt):
# Simulate a start date so that 'at_dt' is older than now().
mocker.patch('arrow.arrow.datetime', wraps=datetime)
start_dt = datetime(2019, 4, 10, 14, 0, 0, tzinfo=tzlocal())
arrow.arrow.datetime.now.return_value = (start_dt + timedelta(hours=1))
result = runner.invoke(cli.start, ['a-project', '--at', at_dt], obj=watson)
assert result.exit_code == 0
30 changes: 26 additions & 4 deletions tests/test_watson.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,26 @@ def test_start_nogap(watson):
assert watson.frames[-1].stop == watson.current['start']


def test_start_project_at(watson):
now = arrow.now()
watson.start('foo', start_at=now)
watson.stop()

# Task can't start before the previous task ends
with pytest.raises(WatsonError):
time_str = '1970-01-01T00:00'
time_obj = arrow.get(time_str)
watson.start('foo', start_at=time_obj)

# Task can't start in the future
with pytest.raises(WatsonError):
time_str = '2999-12-31T23:59'
time_obj = arrow.get(time_str)
watson.start('foo', start_at=time_obj)

assert watson.frames[-1].start == now


# stop

def test_stop_started_project(watson):
Expand Down Expand Up @@ -311,13 +331,15 @@ def test_stop_started_project_at(watson):
watson.start('foo')
now = arrow.now()

# Task can't end before it starts
with pytest.raises(WatsonError):
time_str = '1970-01-01T00:00'
time_obj = arrow.get(time_str)
watson.stop(stop_at=time_obj)

with pytest.raises(ValueError):
time_str = '2999-31-12T23:59'
# Task can't end in the future
with pytest.raises(WatsonError):
time_str = '2999-12-31T23:59'
time_obj = arrow.get(time_str)
watson.stop(stop_at=time_obj)

Expand Down Expand Up @@ -635,14 +657,14 @@ def json(self):
{
'id': '1c006c6e-6cc1-4c80-ab22-b51c857c0b06',
'project': 'foo',
'start_at': 4003,
'begin_at': 4003,
'end_at': 4004,
'tags': ['A']
},
{
'id': 'c44aa815-4d77-4a58-bddd-1afa95562141',
'project': 'bar',
'start_at': 4004,
'begin_at': 4004,
'end_at': 4005,
'tags': []
}
Expand Down
32 changes: 26 additions & 6 deletions watson/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,12 @@ 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, start_at=None, gap=True):
"""
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, start_at=start_at,
gap=gap,)
click.echo(u"Starting project {}{} at {}".format(
style('project', project),
(" " if current['tags'] else "") + style('tags', current['tags']),
Expand All @@ -175,7 +176,12 @@ def _start(watson, project, tags, restart=False, gap=True):


@cli.command()
@click.option('--at', 'at_', type=DateTime, default=None,
cls=MutuallyExclusiveOption, mutually_exclusive=['gap_'],
help=('Start frame at this time. Must be in '
'(YYYY-MM-DDT)?HH:MM(:SS)? format.'))
@click.option('-g/-G', '--gap/--no-gap', 'gap_', is_flag=True, default=True,
cls=MutuallyExclusiveOption, mutually_exclusive=['at_'],
help=("(Don't) leave gap between end time of previous project "
"and start time of the current."))
@click.argument('args', nargs=-1,
Expand All @@ -187,7 +193,8 @@ def _start(watson, project, tags, restart=False, gap=True):
@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, at_,
gap_=True):
"""
Start monitoring time for the given project.
You can add tags indicating more specifically what you are working on with
Expand All @@ -197,6 +204,16 @@ def start(ctx, watson, confirm_new_project, confirm_new_tag, args, gap_=True):
`options.stop_on_start` is set to a true value (`1`, `on`, `true`, or
`yes`), it is stopped before the new project is started.
If `--at` option is given, the provided starting time is used. The
specified time must be after the end of the previous frame and must not be
in the future.
Example:
\b
$ watson start --at 13:37
Starting project apollo11 at 13:37
If the `--no-gap` flag is given, the start time of the new project is set
to the stop time of the most recently stopped project.
Expand Down Expand Up @@ -239,7 +256,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, start_at=at_, gap=gap_)


@cli.command(context_settings={'ignore_unknown_options': True})
Expand Down Expand Up @@ -275,13 +292,16 @@ def stop(watson, at_):


@cli.command(context_settings={'ignore_unknown_options': True})
@click.option('--at', 'at_', type=DateTime, default=None,
help=('Start frame at this time. Must be in '
'(YYYY-MM-DDT)?HH:MM(:SS)? format.'))
@click.option('-s/-S', '--stop/--no-stop', 'stop_', default=None,
help="(Don't) Stop an already running project.")
@click.argument('frame', default='-1', autocompletion=get_frames)
@click.pass_obj
@click.pass_context
@catch_watson_error
def restart(ctx, watson, frame, stop_):
def restart(ctx, watson, frame, stop_, at_):
"""
Restart monitoring time for a previously stopped project.
Expand Down Expand Up @@ -329,7 +349,7 @@ def restart(ctx, watson, frame, stop_):

frame = get_frame_from_argument(watson, frame)

_start(watson, frame.project, frame.tags, restart=True)
_start(watson, frame.project, frame.tags, restart=True, start_at=at_)


@cli.command()
Expand Down
20 changes: 17 additions & 3 deletions watson/watson.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ 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, start_at=None,
gap=True):
if self.is_started:
raise WatsonError(
u"Project {} is already started.".format(
Expand All @@ -263,7 +264,20 @@ def start(self, project, tags=None, restart=False, gap=True):
if not restart:
tags = (tags or []) + default_tags

if start_at is None:
start_at = arrow.now()
elif self.frames:
# Only perform this check if an explicit start time was given
# and previous frames exist
stop_of_prev_frame = self.frames[-1].stop
if start_at < stop_of_prev_frame:
raise WatsonError('Task cannot start before the previous task '
'ends.')
if start_at > arrow.now():
raise WatsonError('Task cannot start in the future.')

new_frame = {'project': project, 'tags': deduplicate(tags)}
new_frame['start'] = start_at
if not gap:
stop_of_prev_frame = self.frames[-1].stop
new_frame['start'] = stop_of_prev_frame
Expand Down Expand Up @@ -385,7 +399,7 @@ def pull(self):
frame_id = uuid.UUID(frame['id']).hex
self.frames[frame_id] = (
frame['project'],
frame['start_at'],
frame['begin_at'],
frame['end_at'],
frame['tags']
)
Expand All @@ -402,7 +416,7 @@ def push(self, last_pull):
if last_pull > frame.updated_at > self.last_sync:
frames.append({
'id': uuid.UUID(frame.id).urn,
'start_at': str(frame.start.to('utc')),
'begin_at': str(frame.start.to('utc')),
'end_at': str(frame.stop.to('utc')),
'project': frame.project,
'tags': frame.tags
Expand Down

0 comments on commit 2cb871e

Please sign in to comment.