From 45bae2c2b39e37d73cfeeb724c6566fd285e5291 Mon Sep 17 00:00:00 2001 From: Marcin Swiderski Date: Sun, 12 Dec 2021 16:18:35 +0100 Subject: [PATCH 1/7] Support for night owls: starting a day at later hour, e.g. 6, to properly report late night work --- docs/user-guide/configuration.md | 1 + watson/cli.py | 3 ++- watson/frames.py | 12 ++++++------ watson/watson.py | 3 ++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index fbccd93..4a49e5b 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -247,6 +247,7 @@ stop_on_restart = false date_format = %Y.%m.%d time_format = %H:%M:%S%z week_start = monday +day_start_hour = 0 log_current = false pager = true report_current = false diff --git a/watson/cli.py b/watson/cli.py index 31b5887..da26f4f 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -1088,7 +1088,8 @@ def log(watson, current, reverse, from_, to, projects, tags, ignore_projects, if reverse is None: reverse = watson.config.getboolean('options', 'reverse_log', True) - span = watson.frames.span(from_, to) + day_start_hour = watson.config.getint('options', 'day_start_hour', 0) + span = watson.frames.span(from_, to, day_start_hour) filtered_frames = watson.frames.filter( projects=projects or None, tags=tags or None, ignore_projects=ignore_projects or None, diff --git a/watson/frames.py b/watson/frames.py index 0e51157..a195de5 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -61,10 +61,10 @@ def __gte__(self, other): class Span(object): - def __init__(self, start, stop, timeframe='day'): - self.timeframe = timeframe - self.start = start.floor(self.timeframe) - self.stop = stop.ceil(self.timeframe) + def __init__(self, start, stop, shift_hours): + timeframe = 'day' + self.start = start.floor(timeframe).shift(hours=shift_hours) + self.stop = stop.ceil(timeframe).shift(hours=shift_hours) def overlaps(self, frame): return frame.start <= self.stop and frame.stop >= self.start @@ -183,5 +183,5 @@ def filter( stop = span.stop if frame.stop > span.stop else frame.stop yield frame._replace(start=start, stop=stop) - def span(self, start, stop): - return Span(start, stop) + def span(self, start, stop, shift_hours): + return Span(start, stop, shift_hours) diff --git a/watson/watson.py b/watson/watson.py index 3f39be2..beb4f66 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -551,7 +551,8 @@ def report(self, from_, to, current=None, projects=None, tags=None, self.frames.add(cur['project'], cur['start'], arrow.utcnow(), cur['tags'], id="current") - span = self.frames.span(from_, to) + day_start_hour = self.config.getint('options', 'day_start_hour', 0) + span = self.frames.span(from_, to, day_start_hour) frames_by_project = sorted_groupby( self.frames.filter( From 9b9474f367c288fd9389aeb3808c81048c3adac5 Mon Sep 17 00:00:00 2001 From: Marcin Swiderski Date: Tue, 14 Dec 2021 00:34:18 +0100 Subject: [PATCH 2/7] Better handling of --day, --week, etc arguments with custom day_start_hour --- watson/cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/watson/cli.py b/watson/cli.py index da26f4f..bf17b96 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -105,6 +105,10 @@ def convert(self, value, param, ctx) -> arrow: "options", "week_start", "monday") date = apply_weekday_offset( start_time=date, week_start=week_start) + if param.name in ["day", "week", "month", "luna", "year"]: + day_start_hour = ctx.obj.config.get( + "options", "day_start_hour", 0) + date = date.shift(hours=-int(day_start_hour)) return date def _parse_multiformat(self, value) -> arrow: From e0ff01e90c45e8c7e1f165d2ff56834174fe1e34 Mon Sep 17 00:00:00 2001 From: Marcin Swiderski Date: Tue, 14 Dec 2021 16:02:20 +0100 Subject: [PATCH 3/7] Revert "Better handling of --day, --week, etc arguments with custom day_start_hour" This reverts commit 3214d4ca12cdd20e8f72546e682cc9bf715b6a05. --- watson/cli.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index bf17b96..da26f4f 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -105,10 +105,6 @@ def convert(self, value, param, ctx) -> arrow: "options", "week_start", "monday") date = apply_weekday_offset( start_time=date, week_start=week_start) - if param.name in ["day", "week", "month", "luna", "year"]: - day_start_hour = ctx.obj.config.get( - "options", "day_start_hour", 0) - date = date.shift(hours=-int(day_start_hour)) return date def _parse_multiformat(self, value) -> arrow: From 8f55ee096cfc531a3546699da10e01a7f447be88 Mon Sep 17 00:00:00 2001 From: Marcin Swiderski Date: Thu, 21 Nov 2024 04:55:40 +0100 Subject: [PATCH 4/7] * Fix day_start_hour support (in a hacky way probably) --- watson/cli.py | 7 +++++-- watson/frames.py | 28 +++++++++++++++++++++------- watson/watson.py | 13 +++++++++---- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index da26f4f..dfcf54e 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -1066,6 +1066,10 @@ def log(watson, current, reverse, from_, to, projects, tags, ignore_projects, if _ is not None): from_ = start_time + if from_ == from_.floor('day'): + hour_shift = watson.config.getint('options', 'day_start_hour', 0) + from_ = from_.shift(hours=hour_shift) + if from_ > to: raise click.ClickException("'from' must be anterior to 'to'") @@ -1088,8 +1092,7 @@ def log(watson, current, reverse, from_, to, projects, tags, ignore_projects, if reverse is None: reverse = watson.config.getboolean('options', 'reverse_log', True) - day_start_hour = watson.config.getint('options', 'day_start_hour', 0) - span = watson.frames.span(from_, to, day_start_hour) + span = watson.frames.span(from_, to) filtered_frames = watson.frames.filter( projects=projects or None, tags=tags or None, ignore_projects=ignore_projects or None, diff --git a/watson/frames.py b/watson/frames.py index a195de5..26c58a0 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -7,6 +7,20 @@ HEADERS = ('start', 'stop', 'project', 'id', 'tags', 'updated_at') +hour_shift = 0 +def set_hour_shift(hour): + global hour_shift + hour_shift = hour + + +def shifted_floor(time, timeframe): + return time.shift(hours=-hour_shift).floor(timeframe).shift(hours=hour_shift) + + +def shifted_ceil(time, timeframe): + return time.shift(hours=-hour_shift).ceil(timeframe).shift(hours=hour_shift) + + class Frame(namedtuple('Frame', HEADERS)): def __new__(cls, start, stop, project, id, tags=None, updated_at=None,): try: @@ -45,7 +59,7 @@ def dump(self): @property def day(self): - return self.start.floor('day') + return shifted_floor(self.start, 'day') def __lt__(self, other): return self.start < other.start @@ -61,10 +75,10 @@ def __gte__(self, other): class Span(object): - def __init__(self, start, stop, shift_hours): - timeframe = 'day' - self.start = start.floor(timeframe).shift(hours=shift_hours) - self.stop = stop.ceil(timeframe).shift(hours=shift_hours) + def __init__(self, start, stop, timeframe = 'day'): + self.timeframe = timeframe + self.start = shifted_floor(start, self.timeframe) + self.stop = shifted_ceil(stop, self.timeframe) def overlaps(self, frame): return frame.start <= self.stop and frame.stop >= self.start @@ -183,5 +197,5 @@ def filter( stop = span.stop if frame.stop > span.stop else frame.stop yield frame._replace(start=start, stop=stop) - def span(self, start, stop, shift_hours): - return Span(start, stop, shift_hours) + def span(self, start, stop): + return Span(start, stop) diff --git a/watson/watson.py b/watson/watson.py index beb4f66..b545ec9 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -10,7 +10,7 @@ import subprocess from .config import ConfigParser -from .frames import Frames +from .frames import Frames, set_hour_shift from .utils import deduplicate, make_json_writer, safe_save, sorted_groupby from .version import version as __version__ # noqa @@ -66,6 +66,8 @@ def __init__(self, **kwargs): if 'last_sync' in kwargs: self.last_sync = kwargs['last_sync'] + set_hour_shift(self.config.getint('options', 'day_start_hour', 0)) + def _load_json_file(self, filename, type=dict): """ Return the content of the the given JSON file. @@ -533,6 +535,10 @@ def report(self, from_, to, current=None, projects=None, tags=None, if _ is not None): from_ = start_time + if from_ == from_.floor('day'): + hour_shift = self.config.getint('options', 'day_start_hour', 0) + from_ = from_.shift(hours=hour_shift) + if not self._validate_report_options(projects, ignore_projects): raise WatsonError( "given projects can't be ignored at the same time") @@ -551,9 +557,8 @@ def report(self, from_, to, current=None, projects=None, tags=None, self.frames.add(cur['project'], cur['start'], arrow.utcnow(), cur['tags'], id="current") - day_start_hour = self.config.getint('options', 'day_start_hour', 0) - span = self.frames.span(from_, to, day_start_hour) - + span = self.frames.span(from_, to) + frames_by_project = sorted_groupby( self.frames.filter( projects=projects or None, tags=tags or None, From 53fa8985c2e387d30da1779eff292a1ea24e9a29 Mon Sep 17 00:00:00 2001 From: Marcin Swiderski Date: Mon, 25 Nov 2024 00:52:09 +0100 Subject: [PATCH 5/7] * Fix handling report/log 'from_' shifting by 'day_start_hour' --- watson/cli.py | 5 ++--- watson/watson.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index 886a847..81910c5 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -19,7 +19,7 @@ get_rename_types, get_tags, ) -from .frames import Frame +from .frames import Frame, shifted_floor from .utils import ( apply_weekday_offset, build_csv, @@ -1067,8 +1067,7 @@ def log(watson, current, reverse, from_, to, projects, tags, ignore_projects, from_ = start_time if from_ == from_.floor('day'): - hour_shift = watson.config.getint('options', 'day_start_hour', 0) - from_ = from_.shift(hours=hour_shift) + from_ = shifted_floor(from_, 'day') if from_ > to: raise click.ClickException("'from' must be anterior to 'to'") diff --git a/watson/watson.py b/watson/watson.py index b545ec9..dfffe5a 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -10,7 +10,7 @@ import subprocess from .config import ConfigParser -from .frames import Frames, set_hour_shift +from .frames import Frames, set_hour_shift, shifted_floor from .utils import deduplicate, make_json_writer, safe_save, sorted_groupby from .version import version as __version__ # noqa @@ -536,8 +536,7 @@ def report(self, from_, to, current=None, projects=None, tags=None, from_ = start_time if from_ == from_.floor('day'): - hour_shift = self.config.getint('options', 'day_start_hour', 0) - from_ = from_.shift(hours=hour_shift) + from_ = shifted_floor(from_, 'day') if not self._validate_report_options(projects, ignore_projects): raise WatsonError( From 94f2fb46149bb08e0fe2ce0d00f35d12701ecd31 Mon Sep 17 00:00:00 2001 From: Marcin Swiderski Date: Sat, 30 Nov 2024 16:40:57 +0100 Subject: [PATCH 6/7] * Fix shifting from_ parameter for the nth time. It's simple math, it shouldn't be so hard XD --- watson/cli.py | 5 ++--- watson/frames.py | 8 ++++++++ watson/watson.py | 5 ++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index 81910c5..a21ff61 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -19,7 +19,7 @@ get_rename_types, get_tags, ) -from .frames import Frame, shifted_floor +from .frames import Frame, shifted_from from .utils import ( apply_weekday_offset, build_csv, @@ -1066,8 +1066,7 @@ def log(watson, current, reverse, from_, to, projects, tags, ignore_projects, if _ is not None): from_ = start_time - if from_ == from_.floor('day'): - from_ = shifted_floor(from_, 'day') + from_ = shifted_from(from_) if from_ > to: raise click.ClickException("'from' must be anterior to 'to'") diff --git a/watson/frames.py b/watson/frames.py index 26c58a0..4a62680 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -21,6 +21,14 @@ def shifted_ceil(time, timeframe): return time.shift(hours=-hour_shift).ceil(timeframe).shift(hours=hour_shift) +def shifted_from(from_): + if from_ == from_.floor('day'): + if arrow.now().hour < hour_shift: + from_ = from_.shift(days=-1) + from_ = from_.shift(hours=hour_shift) + return from_ + + class Frame(namedtuple('Frame', HEADERS)): def __new__(cls, start, stop, project, id, tags=None, updated_at=None,): try: diff --git a/watson/watson.py b/watson/watson.py index dfffe5a..764d0bb 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -10,7 +10,7 @@ import subprocess from .config import ConfigParser -from .frames import Frames, set_hour_shift, shifted_floor +from .frames import Frames, set_hour_shift, shifted_from from .utils import deduplicate, make_json_writer, safe_save, sorted_groupby from .version import version as __version__ # noqa @@ -535,8 +535,7 @@ def report(self, from_, to, current=None, projects=None, tags=None, if _ is not None): from_ = start_time - if from_ == from_.floor('day'): - from_ = shifted_floor(from_, 'day') + from_ = shifted_from(from_) if not self._validate_report_options(projects, ignore_projects): raise WatsonError( From 64c7eed0f68db5cc94109f0527827a2ccd4f5298 Mon Sep 17 00:00:00 2001 From: Marcin Swiderski Date: Sun, 8 Dec 2024 17:40:57 +0100 Subject: [PATCH 7/7] * Fix code formatting --- watson/frames.py | 14 +++++++++++--- watson/watson.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/watson/frames.py b/watson/frames.py index 4a62680..1c329c0 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -8,17 +8,25 @@ hour_shift = 0 + + def set_hour_shift(hour): global hour_shift hour_shift = hour def shifted_floor(time, timeframe): - return time.shift(hours=-hour_shift).floor(timeframe).shift(hours=hour_shift) + time = time.shift(hours=-hour_shift) + time = time.floor(timeframe) + time = time.shift(hours=hour_shift) + return time def shifted_ceil(time, timeframe): - return time.shift(hours=-hour_shift).ceil(timeframe).shift(hours=hour_shift) + time = time.shift(hours=-hour_shift) + time = time.ceil(timeframe) + time = time.shift(hours=hour_shift) + return time def shifted_from(from_): @@ -83,7 +91,7 @@ def __gte__(self, other): class Span(object): - def __init__(self, start, stop, timeframe = 'day'): + def __init__(self, start, stop, timeframe='day'): self.timeframe = timeframe self.start = shifted_floor(start, self.timeframe) self.stop = shifted_ceil(stop, self.timeframe) diff --git a/watson/watson.py b/watson/watson.py index 764d0bb..b0db440 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -556,7 +556,7 @@ def report(self, from_, to, current=None, projects=None, tags=None, cur['tags'], id="current") span = self.frames.span(from_, to) - + frames_by_project = sorted_groupby( self.frames.filter( projects=projects or None, tags=tags or None,