From 497b44a6bd2fd045ffc3a819d31cac45bc53652a Mon Sep 17 00:00:00 2001 From: dotcs Date: Thu, 3 Mar 2022 22:34:13 +0100 Subject: [PATCH 1/5] Migrate CLI commands to typer (WIP) --- requirements.txt | 3 +- worklog/cmd/__init__.py | 174 ++++++++++++++++++++++++++++++++++++++++ worklog/cmd/session.py | 53 ++++++++++++ worklog/cmd/status.py | 7 ++ worklog/cmd/task.py | 76 ++++++++++++++++++ worklog/cmd/utils.py | 87 ++++++++++++++++++++ worklog/constants.py | 6 ++ worklog/log.py | 4 +- 8 files changed, 406 insertions(+), 4 deletions(-) create mode 100644 worklog/cmd/__init__.py create mode 100644 worklog/cmd/session.py create mode 100644 worklog/cmd/status.py create mode 100644 worklog/cmd/task.py create mode 100644 worklog/cmd/utils.py diff --git a/requirements.txt b/requirements.txt index 1411a4a..af50ce2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -pandas \ No newline at end of file +pandas +typer diff --git a/worklog/cmd/__init__.py b/worklog/cmd/__init__.py new file mode 100644 index 0000000..1d6c910 --- /dev/null +++ b/worklog/cmd/__init__.py @@ -0,0 +1,174 @@ +import typer +from typing import Optional +from datetime import datetime, timezone, timedelta +import re + +from worklog.cmd.doctor import app as doctor_app +from worklog.cmd.log import app as log_app +from worklog.cmd.report import app as report_app +from worklog.cmd.session import app as session_app +from worklog.cmd.status import app as status_app +from worklog.cmd.task import app as task_app +from worklog.cmd.utils import configure_worklog +from worklog.constants import Category +import worklog.constants as wc + +app = typer.Typer() + +now = datetime.now(timezone.utc).astimezone(tz=wc.LOCAL_TIMEZONE).replace(microsecond=0) +current_month: str = now.replace(day=1).isoformat()[: len("2000-01-01")] +next_month: str = (now.replace(day=1) + timedelta(days=31)).replace(day=1).isoformat()[ + : len("2000-01-01") +] + + +def _positive_int(value: int): + if value < 0: + raise typer.BadParameter("Value must be larger or equal to zero") + return value + + +def _combined_month_or_day_or_week_parser(value: str) -> str: + if re.match(r"^\d{4}\-\d{2}$", value): + return _year_month_parser(value).isoformat() + elif re.match(r"^\d{4}\-\d{2}\-\d{2}$", value): + return _year_month_day_parser(value).isoformat() + elif re.match(r"^\d{4}-W\d{2}$", value): + return _calendar_week_parser(value).isoformat() + raise typer.BadParameter(f"{value} is not a valid format") + + +def _year_month_parser(value: str) -> datetime: + if not re.match(r"^\d{4}\-\d{2}$", value): + raise typer.BadParameter(f"{value} is not in the format YYYY-MM") + year, month = [int(x) for x in value.split("-")] + return datetime(year=year, month=month, day=1, tzinfo=wc.LOCAL_TIMEZONE) + + +def _year_month_day_parser(value: str) -> datetime: + if not re.match(r"^\d{4}\-\d{2}\-\d{2}$", value): + raise typer.BadParameter(f"{value} is not in the format YYYY-MM-DD") + year, month, day = [int(x) for x in value.split("-")] + return datetime(year=year, month=month, day=day, tzinfo=wc.LOCAL_TIMEZONE) + + +def _calendar_week_parser(value: str) -> datetime: + if not re.match(r"^\d{4}-W\d{2}$", value): + raise typer.BadParameter(f"{value} is not in the format cwWW") + dt = datetime.strptime(value + "-1", "%Y-W%W-%w").replace(tzinfo=wc.LOCAL_TIMEZONE) + return dt + + +@app.callback() +def callback(): + """Simple CLI tool to log work and projects.""" + + +@app.command() +def doctor(): + """ + The doctor command checks the worklog for missing or problematic entries. \ + It will report the following issues: non-closed working sessions. + """ + log, _ = configure_worklog() + log.doctor() + + +@app.command() +def log( + number: int = typer.Option( + 10, + "--number", + "-n", + help="Defines many log entries should be shown. System pager will be used if n > 20.", + callback=_positive_int, + ), + show_all: bool = typer.Option( + False, "--all", "-a", help="Show all entries. System pager will be used.", + ), + category: Category = typer.Option(None, help="Filter category"), + pager: bool = typer.Option( + True, + help=( + "Use a the system pager. " + "Prints all output to STDOUT regardless of how many entries will be shown. " + "This flag should be used if there are problems with the system pager." + ), + ), +): + """Shows the content of the worklog file sorted after the date and time of \ + the entry. Use this command to manually review the content of the \ + worklog.""" + log, cfg = configure_worklog() + no_pager_max_entries = int(cfg.get("worklog", "no_pager_max_entries")) + use_pager = pager and (show_all or number > no_pager_max_entries) + category_str: Optional[str] = None if not category else category.value + if not show_all: + log.log(number, use_pager, category_str) + else: + log.log(-1, use_pager, category_str) + + +@app.command() +def report( + date_from: str = typer.Option( + current_month, + callback=_combined_month_or_day_or_week_parser, + help=( + "Date from which the aggregation should be started (inclusive). " + "By default the start of the current calendar month is selected. " + "Allowed input formats are YYYY-MM-DD, YYYY-MM and YYYY-WXX, with " + "XX referring to the week number, e.g. 35." + ), + ), + date_to: str = typer.Option( + next_month, + callback=_combined_month_or_day_or_week_parser, + help=( + "Date to which the aggregation should be started (exclusive). " + "By default the next calendar month is selected. " + "Allowed input formats are YYYY-MM-DD, YYYY-MM and YYYY-WXX, with " + "XX referring to the week number, e.g. 35." + ), + ), +): + """ + Creates a report for a given time window. Working time will be aggregated on a \ + monthly, weekly and daily basis. Tasks will be aggregated separately. By \ + default the current month will be used for the report. + """ + log, _ = configure_worklog() + dt_date_from = datetime.fromisoformat(date_from) + dt_date_to = datetime.fromisoformat(date_to) + log.report(dt_date_from, dt_date_to) + + +app.add_typer( + session_app, + name="session", + help="""\ + Commit the start or end of a new working session to the worklog file. \ + Use this function to stamp in the morning and stamp out in the \ + evening.""", +) +app.add_typer( + status_app, + name="status", + help="""\ + Creates a report for a given time window. Working time will be aggregated on \ + a monthly, weekly and daily basis. Tasks will be aggregated separately. By \ + default the current month will be used for the report. + """, +) +app.add_typer( + task_app, + name="task", + help="""\ + Tasks are pieces of work to be done or undertaken. A task can only be \ + started during an ongoing session. Use 'wl session start' to start a new \ + working session.""", +) + + +if __name__ == "__main__": + app() diff --git a/worklog/cmd/session.py b/worklog/cmd/session.py new file mode 100644 index 0000000..fbdb3ae --- /dev/null +++ b/worklog/cmd/session.py @@ -0,0 +1,53 @@ +import typer +from typing import Optional + +from worklog.cmd.utils import ( + MutuallyExclusiveGroup, + configure_worklog, + offset_minutes_opt, + time_opt, +) +import worklog.constants as wc + +app = typer.Typer() + + +_force_opt = typer.Option( + False, "--force", "-f", help="Force command, will auto-stop running tasks." +) + + +@app.command() +def start( + force: bool = _force_opt, + offset_minutes: Optional[int] = offset_minutes_opt, + time: Optional[str] = time_opt, +): + log, cfg = configure_worklog() + log.commit( + wc.TOKEN_SESSION, + wc.TOKEN_START, + offset_min=0 if offset_minutes is None else offset_minutes, + time=time, + force=force, + ) + + +@app.command() +def stop( + force: bool = _force_opt, + offset_minutes: Optional[int] = offset_minutes_opt, + time: Optional[str] = time_opt, +): + log, cfg = configure_worklog() + log.commit( + wc.TOKEN_SESSION, + wc.TOKEN_STOP, + offset_min=0 if offset_minutes is None else offset_minutes, + time=time, + force=force, + ) + + +if __name__ == "__main__": + app() diff --git a/worklog/cmd/status.py b/worklog/cmd/status.py new file mode 100644 index 0000000..e85240a --- /dev/null +++ b/worklog/cmd/status.py @@ -0,0 +1,7 @@ +import typer + +app = typer.Typer() + + +if __name__ == "__main__": + app() diff --git a/worklog/cmd/task.py b/worklog/cmd/task.py new file mode 100644 index 0000000..b7cce29 --- /dev/null +++ b/worklog/cmd/task.py @@ -0,0 +1,76 @@ +import typer +from typing import Optional + +from worklog.cmd.utils import ( + configure_worklog, + offset_minutes_opt, + time_opt, + configure_worklog, +) +from worklog.utils.time import calc_log_time +import worklog.constants as wc + +app = typer.Typer() + + +@app.command() +def start( + task_id: str = typer.Argument(..., help="Task identifier, can be freely chosen"), + auto_stop: bool = typer.Option( + False, "--auto-stop", "-as", help="Automatically stops open tasks.", + ), + offset_minutes: Optional[int] = offset_minutes_opt, + time: Optional[str] = time_opt, +): + log, _ = configure_worklog() + + offset_min = 0 if offset_minutes is None else offset_minutes + + if auto_stop: + commit_dt = calc_log_time(offset_min, time) + log.stop_active_tasks(commit_dt) + + log.commit( + wc.TOKEN_TASK, + wc.TOKEN_START, + offset_min=offset_min, + time=time, + identifier=task_id, + ) + + +@app.command() +def stop( + task_id: str = typer.Argument(..., help="Task identifier of a running task"), + offset_minutes: Optional[int] = offset_minutes_opt, + time: Optional[str] = time_opt, +): + log, _ = configure_worklog() + + offset_min = 0 if offset_minutes is None else offset_minutes + + log.commit( + wc.TOKEN_TASK, + wc.TOKEN_STOP, + offset_min=offset_min, + time=time, + identifier=task_id, + ) + + +@app.command() +def list(): + log, _ = configure_worklog() + log.list_tasks() + + +@app.command() +def report( + task_id: str = typer.Argument(..., help="Task identifier of a recorded task"), +): + log, _ = configure_worklog() + log.task_report(task_id) + + +if __name__ == "__main__": + app() diff --git a/worklog/cmd/utils.py b/worklog/cmd/utils.py new file mode 100644 index 0000000..b521823 --- /dev/null +++ b/worklog/cmd/utils.py @@ -0,0 +1,87 @@ +import os +from configparser import ConfigParser +from io import StringIO +import json +from typing import Tuple, Optional +import typer + +from worklog.breaks import AutoBreak +import worklog.constants as wc +from worklog.log import Log +from worklog.parser import get_arg_parser +from worklog.utils.time import calc_log_time +from worklog.utils.logger import configure_logger +from worklog.dispatcher import dispatch + + +def configure_worklog() -> Tuple[Log, ConfigParser]: + """ Main method """ + logger = configure_logger() + + # log_level = wc.LOG_LEVELS[min(cli_args.verbosity, len(wc.LOG_LEVELS) - 1)] + # logger.setLevel(log_level) + + # logger.debug(f"Parsed CLI arguments: {cli_args}") + # logger.debug(f"Path to config files: {wc.CONFIG_FILES}") + + # if cli_args.subcmd is None: + # parser.print_help() + # return + + cfg = ConfigParser() + cfg.read(wc.CONFIG_FILES) + + with StringIO() as ss: + cfg.write(ss) + ss.seek(0) + logger.debug(f"Config content:\n{ss.read()}\nEOF") + + worklog_fp = os.path.expanduser(cfg.get("worklog", "path")) + log = Log(worklog_fp) + + limits = json.loads(cfg.get("workday", "auto_break_limit_minutes")) + durations = json.loads(cfg.get("workday", "auto_break_duration_minutes")) + log.auto_break = AutoBreak(limits, durations) + + return log, cfg + + +# Taken from https://github.com/tiangolo/typer/issues/140#issuecomment-898937671 +def MutuallyExclusiveGroup(size=2): + group = set() + + def callback(ctx: typer.Context, param: typer.CallbackParam, value: str): + # Add cli option to group if it was called with a value + if value is not None and param.name not in group: + group.add(param.name) + if len(group) > size - 1: + raise typer.BadParameter( + f"{param.name} is mutually exclusive with {group.pop()}" + ) + return value + + return callback + + +timeshift_grp = MutuallyExclusiveGroup() +offset_minutes_opt: Optional[int] = typer.Option( + None, + "--offset-minutes", + "-om", + callback=timeshift_grp, + help=( + "Offset of the start/stop time in minutes. " + "Positive values shift the timestamp into the future, negative " + "values shift it into the past." + ), +) +time_opt: Optional[str] = typer.Option( + None, + callback=timeshift_grp, + help=( + "Exact point in time. " + "Can be a either hours and minutes (format: 'hh:mm') on the same day or a full ISO " + "format string, such as '2020-08-05T08:15:00+02:00'. " + "In the latter case the local timezone is used if the timezone part is left empty." + ), +) diff --git a/worklog/constants.py b/worklog/constants.py index dbc1341..a00c883 100644 --- a/worklog/constants.py +++ b/worklog/constants.py @@ -2,6 +2,7 @@ import logging import os from datetime import datetime, timezone, tzinfo +import enum LOG_FORMAT: str = logging.BASIC_FORMAT LOG_LEVELS: List[int] = [logging.ERROR, logging.WARN, logging.INFO, logging.DEBUG] @@ -35,3 +36,8 @@ TOKEN_SESSION = "session" TOKEN_TASK = "task" + +class Category(enum.Enum): + Session = "session" + Task = "task" + diff --git a/worklog/log.py b/worklog/log.py index ada6ab6..8a58e03 100644 --- a/worklog/log.py +++ b/worklog/log.py @@ -108,9 +108,7 @@ def list_tasks(self): count = task_counter[task] sys.stdout.write(f"{task} ({count})\n") - def log( - self, n: int, use_pager: bool, filter_category: Optional[List[str]] - ) -> None: + def log(self, n: int, use_pager: bool, filter_category: Optional[str]) -> None: """Display the content of the logfile.""" if self._log_df.shape[0] == 0: sys.stdout.write("No data available\n") From 72f96a1c125297d9332ed76bf9be8fbf42599c80 Mon Sep 17 00:00:00 2001 From: dotcs Date: Tue, 8 Mar 2022 18:27:21 +0100 Subject: [PATCH 2/5] Fix wrong imports --- worklog/cmd/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/worklog/cmd/__init__.py b/worklog/cmd/__init__.py index 1d6c910..d098fbc 100644 --- a/worklog/cmd/__init__.py +++ b/worklog/cmd/__init__.py @@ -3,9 +3,6 @@ from datetime import datetime, timezone, timedelta import re -from worklog.cmd.doctor import app as doctor_app -from worklog.cmd.log import app as log_app -from worklog.cmd.report import app as report_app from worklog.cmd.session import app as session_app from worklog.cmd.status import app as status_app from worklog.cmd.task import app as task_app From 61694190f6b1ddf975ef721a6597ef4cd6fd49cd Mon Sep 17 00:00:00 2001 From: dotcs Date: Thu, 10 Mar 2022 21:01:10 +0100 Subject: [PATCH 3/5] Add msgs on stdout when commiting a session/task --- worklog/cmd/session.py | 10 ++++++++-- worklog/cmd/task.py | 28 ++++++++++++++++++++++++---- worklog/cmd/utils.py | 7 +++++++ worklog/log.py | 6 ++++-- worklog/utils/tasks.py | 2 +- 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/worklog/cmd/session.py b/worklog/cmd/session.py index fbdb3ae..9bff9ee 100644 --- a/worklog/cmd/session.py +++ b/worklog/cmd/session.py @@ -1,11 +1,13 @@ import typer from typing import Optional +from datetime import datetime from worklog.cmd.utils import ( MutuallyExclusiveGroup, configure_worklog, offset_minutes_opt, time_opt, + stdout_log_entry_date_fmt, ) import worklog.constants as wc @@ -24,13 +26,15 @@ def start( time: Optional[str] = time_opt, ): log, cfg = configure_worklog() - log.commit( + dt = log.commit( wc.TOKEN_SESSION, wc.TOKEN_START, offset_min=0 if offset_minutes is None else offset_minutes, time=time, force=force, ) + fmt = stdout_log_entry_date_fmt(dt) + typer.echo("Session started on {date}".format(date=dt.strftime(fmt))) @app.command() @@ -40,13 +44,15 @@ def stop( time: Optional[str] = time_opt, ): log, cfg = configure_worklog() - log.commit( + dt = log.commit( wc.TOKEN_SESSION, wc.TOKEN_STOP, offset_min=0 if offset_minutes is None else offset_minutes, time=time, force=force, ) + fmt = stdout_log_entry_date_fmt(dt) + typer.echo("Session stopped on {date}".format(date=dt.strftime(fmt))) if __name__ == "__main__": diff --git a/worklog/cmd/task.py b/worklog/cmd/task.py index b7cce29..0c3bf00 100644 --- a/worklog/cmd/task.py +++ b/worklog/cmd/task.py @@ -6,6 +6,7 @@ offset_minutes_opt, time_opt, configure_worklog, + stdout_log_entry_date_fmt, ) from worklog.utils.time import calc_log_time import worklog.constants as wc @@ -28,15 +29,28 @@ def start( if auto_stop: commit_dt = calc_log_time(offset_min, time) - log.stop_active_tasks(commit_dt) - - log.commit( + stopped_tasks = log.stop_active_tasks(commit_dt) + fmt = stdout_log_entry_date_fmt(commit_dt) + for task_id in stopped_tasks: + typer.echo( + "Task {task_id} stopped at {date}".format( + date=commit_dt.strftime(fmt), task_id=task_id + ) + ) + + dt = log.commit( wc.TOKEN_TASK, wc.TOKEN_START, offset_min=offset_min, time=time, identifier=task_id, ) + fmt = stdout_log_entry_date_fmt(dt) + typer.echo( + "Task {task_id} started at {date}".format( + date=dt.strftime(fmt), task_id=task_id + ) + ) @app.command() @@ -49,13 +63,19 @@ def stop( offset_min = 0 if offset_minutes is None else offset_minutes - log.commit( + dt = log.commit( wc.TOKEN_TASK, wc.TOKEN_STOP, offset_min=offset_min, time=time, identifier=task_id, ) + fmt = stdout_log_entry_date_fmt(dt) + typer.echo( + "Task {task_id} stopped at {date}".format( + date=dt.strftime(fmt), task_id=task_id + ) + ) @app.command() diff --git a/worklog/cmd/utils.py b/worklog/cmd/utils.py index b521823..11f8278 100644 --- a/worklog/cmd/utils.py +++ b/worklog/cmd/utils.py @@ -4,6 +4,7 @@ import json from typing import Tuple, Optional import typer +from datetime import datetime from worklog.breaks import AutoBreak import worklog.constants as wc @@ -85,3 +86,9 @@ def callback(ctx: typer.Context, param: typer.CallbackParam, value: str): "In the latter case the local timezone is used if the timezone part is left empty." ), ) + + +def stdout_log_entry_date_fmt(dt: datetime) -> str: + today = datetime.today().date() + fmt = "%H:%M:%S" if dt.date() == today else "%Y-%m-%d %H:%M:%S" + return fmt diff --git a/worklog/log.py b/worklog/log.py index 8a58e03..64fcafc 100644 --- a/worklog/log.py +++ b/worklog/log.py @@ -74,10 +74,11 @@ def commit( time: Optional[str] = None, identifier: str = None, force: bool = False, - ) -> None: + ) -> datetime: """Commit a session/task change to the logfile.""" log_date = calc_log_time(offset_min, time) self._commit(category, type_, log_date, identifier, force) + return log_date def doctor(self) -> None: """Test if the logfile is consistent.""" @@ -272,7 +273,7 @@ def status( ) ) - def stop_active_tasks(self, log_dt: datetime): + def stop_active_tasks(self, log_dt: datetime) -> List[str]: """Stop all active tasks by commiting changes to the logfile.""" query_date = log_dt.date() task_mask = self._log_df[wc.COL_CATEGORY] == wc.TOKEN_TASK @@ -281,6 +282,7 @@ def stop_active_tasks(self, log_dt: datetime): active_task_ids = get_active_task_ids(self._log_df[mask]) for task_id in active_task_ids: self._commit(wc.TOKEN_TASK, wc.TOKEN_STOP, log_dt, identifier=task_id) + return active_task_ids def task_report(self, task_id): """Generate a report of a given task.""" diff --git a/worklog/utils/tasks.py b/worklog/utils/tasks.py index 0f7e542..d0fe382 100644 --- a/worklog/utils/tasks.py +++ b/worklog/utils/tasks.py @@ -46,7 +46,7 @@ def get_all_task_ids_with_duration(df: DataFrame): return s.to_dict() -def get_active_task_ids(df: DataFrame): +def get_active_task_ids(df: DataFrame) -> List[str]: """ Returns a list of active tasks. Note: This method can only handle DataFrames that have entries where the From c669b71c420c0931b168a54ead80e202f8a2845b Mon Sep 17 00:00:00 2001 From: dotcs Date: Thu, 10 Mar 2022 21:08:49 +0100 Subject: [PATCH 4/5] Don't overwrite variable 'task_id' --- worklog/cmd/task.py | 4 ++-- worklog/log.py | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/worklog/cmd/task.py b/worklog/cmd/task.py index 0c3bf00..3318ac5 100644 --- a/worklog/cmd/task.py +++ b/worklog/cmd/task.py @@ -31,10 +31,10 @@ def start( commit_dt = calc_log_time(offset_min, time) stopped_tasks = log.stop_active_tasks(commit_dt) fmt = stdout_log_entry_date_fmt(commit_dt) - for task_id in stopped_tasks: + for tid in stopped_tasks: typer.echo( "Task {task_id} stopped at {date}".format( - date=commit_dt.strftime(fmt), task_id=task_id + date=commit_dt.strftime(fmt), task_id=tid ) ) diff --git a/worklog/log.py b/worklog/log.py index 64fcafc..fea9f2c 100644 --- a/worklog/log.py +++ b/worklog/log.py @@ -274,15 +274,18 @@ def status( ) def stop_active_tasks(self, log_dt: datetime) -> List[str]: - """Stop all active tasks by commiting changes to the logfile.""" + """ + Stop all active tasks by commiting changes to the logfile. + Returns the list of all stopped task ids. + """ query_date = log_dt.date() task_mask = self._log_df[wc.COL_CATEGORY] == wc.TOKEN_TASK date_mask = self._log_df["date"] == query_date mask = task_mask & date_mask - active_task_ids = get_active_task_ids(self._log_df[mask]) - for task_id in active_task_ids: + tasks_to_stop = get_active_task_ids(self._log_df[mask]) + for task_id in tasks_to_stop: self._commit(wc.TOKEN_TASK, wc.TOKEN_STOP, log_dt, identifier=task_id) - return active_task_ids + return tasks_to_stop def task_report(self, task_id): """Generate a report of a given task.""" From 392d3910d179340bb20af91d9572496036569473 Mon Sep 17 00:00:00 2001 From: dotcs Date: Thu, 10 Mar 2022 22:18:30 +0100 Subject: [PATCH 5/5] Use second logger to log commit events --- worklog/cmd/__init__.py | 7 +++++++ worklog/cmd/session.py | 9 ++------- worklog/cmd/task.py | 26 +++----------------------- worklog/cmd/utils.py | 6 ------ worklog/constants.py | 3 +++ worklog/log.py | 17 +++++++++++++---- 6 files changed, 28 insertions(+), 40 deletions(-) diff --git a/worklog/cmd/__init__.py b/worklog/cmd/__init__.py index d098fbc..82180b0 100644 --- a/worklog/cmd/__init__.py +++ b/worklog/cmd/__init__.py @@ -2,6 +2,8 @@ from typing import Optional from datetime import datetime, timezone, timedelta import re +import logging +import sys from worklog.cmd.session import app as session_app from worklog.cmd.status import app as status_app @@ -60,6 +62,11 @@ def _calendar_week_parser(value: str) -> datetime: def callback(): """Simple CLI tool to log work and projects.""" + # Configure the standard logger to output to stdout + std_logger = logging.getLogger(wc.STD_LOGGER_NAME) + std_logger.setLevel(logging.INFO) + std_logger.addHandler(logging.StreamHandler(sys.stdout)) + @app.command() def doctor(): diff --git a/worklog/cmd/session.py b/worklog/cmd/session.py index 9bff9ee..519f6ab 100644 --- a/worklog/cmd/session.py +++ b/worklog/cmd/session.py @@ -7,7 +7,6 @@ configure_worklog, offset_minutes_opt, time_opt, - stdout_log_entry_date_fmt, ) import worklog.constants as wc @@ -26,15 +25,13 @@ def start( time: Optional[str] = time_opt, ): log, cfg = configure_worklog() - dt = log.commit( + log.commit( wc.TOKEN_SESSION, wc.TOKEN_START, offset_min=0 if offset_minutes is None else offset_minutes, time=time, force=force, ) - fmt = stdout_log_entry_date_fmt(dt) - typer.echo("Session started on {date}".format(date=dt.strftime(fmt))) @app.command() @@ -44,15 +41,13 @@ def stop( time: Optional[str] = time_opt, ): log, cfg = configure_worklog() - dt = log.commit( + log.commit( wc.TOKEN_SESSION, wc.TOKEN_STOP, offset_min=0 if offset_minutes is None else offset_minutes, time=time, force=force, ) - fmt = stdout_log_entry_date_fmt(dt) - typer.echo("Session stopped on {date}".format(date=dt.strftime(fmt))) if __name__ == "__main__": diff --git a/worklog/cmd/task.py b/worklog/cmd/task.py index 3318ac5..ae83679 100644 --- a/worklog/cmd/task.py +++ b/worklog/cmd/task.py @@ -6,7 +6,6 @@ offset_minutes_opt, time_opt, configure_worklog, - stdout_log_entry_date_fmt, ) from worklog.utils.time import calc_log_time import worklog.constants as wc @@ -30,27 +29,14 @@ def start( if auto_stop: commit_dt = calc_log_time(offset_min, time) stopped_tasks = log.stop_active_tasks(commit_dt) - fmt = stdout_log_entry_date_fmt(commit_dt) - for tid in stopped_tasks: - typer.echo( - "Task {task_id} stopped at {date}".format( - date=commit_dt.strftime(fmt), task_id=tid - ) - ) - - dt = log.commit( + + log.commit( wc.TOKEN_TASK, wc.TOKEN_START, offset_min=offset_min, time=time, identifier=task_id, ) - fmt = stdout_log_entry_date_fmt(dt) - typer.echo( - "Task {task_id} started at {date}".format( - date=dt.strftime(fmt), task_id=task_id - ) - ) @app.command() @@ -63,19 +49,13 @@ def stop( offset_min = 0 if offset_minutes is None else offset_minutes - dt = log.commit( + log.commit( wc.TOKEN_TASK, wc.TOKEN_STOP, offset_min=offset_min, time=time, identifier=task_id, ) - fmt = stdout_log_entry_date_fmt(dt) - typer.echo( - "Task {task_id} stopped at {date}".format( - date=dt.strftime(fmt), task_id=task_id - ) - ) @app.command() diff --git a/worklog/cmd/utils.py b/worklog/cmd/utils.py index 11f8278..7f7fbd5 100644 --- a/worklog/cmd/utils.py +++ b/worklog/cmd/utils.py @@ -86,9 +86,3 @@ def callback(ctx: typer.Context, param: typer.CallbackParam, value: str): "In the latter case the local timezone is used if the timezone part is left empty." ), ) - - -def stdout_log_entry_date_fmt(dt: datetime) -> str: - today = datetime.today().date() - fmt = "%H:%M:%S" if dt.date() == today else "%Y-%m-%d %H:%M:%S" - return fmt diff --git a/worklog/constants.py b/worklog/constants.py index a00c883..b281332 100644 --- a/worklog/constants.py +++ b/worklog/constants.py @@ -15,6 +15,9 @@ LOCAL_TIMEZONE: Optional[tzinfo] = datetime.now(timezone.utc).astimezone().tzinfo DEFAULT_LOGGER_NAME = "worklog" +"""Logger that can be configured in CLI mode with verbosity flags""" +STD_LOGGER_NAME = "std_logger" +"""Logger that writes always to stdout (in CLI mode)""" SUBCMD_SESSION = "session" SUBCMD_DOCTOR = "doctor" diff --git a/worklog/log.py b/worklog/log.py index fea9f2c..443add6 100644 --- a/worklog/log.py +++ b/worklog/log.py @@ -31,6 +31,8 @@ ) from worklog.errors import ErrMsg +std_logger = logging.getLogger(wc.STD_LOGGER_NAME) + class Log(object): # In-memory representation of log @@ -74,11 +76,10 @@ def commit( time: Optional[str] = None, identifier: str = None, force: bool = False, - ) -> datetime: + ) -> None: """Commit a session/task change to the logfile.""" log_date = calc_log_time(offset_min, time) self._commit(category, type_, log_date, identifier, force) - return log_date def doctor(self) -> None: """Test if the logfile is consistent.""" @@ -273,7 +274,7 @@ def status( ) ) - def stop_active_tasks(self, log_dt: datetime) -> List[str]: + def stop_active_tasks(self, log_dt: datetime): """ Stop all active tasks by commiting changes to the logfile. Returns the list of all stopped task ids. @@ -285,7 +286,6 @@ def stop_active_tasks(self, log_dt: datetime) -> List[str]: tasks_to_stop = get_active_task_ids(self._log_df[mask]) for task_id in tasks_to_stop: self._commit(wc.TOKEN_TASK, wc.TOKEN_STOP, log_dt, identifier=task_id) - return tasks_to_stop def task_report(self, task_id): """Generate a report of a given task.""" @@ -421,6 +421,15 @@ def _commit( # and persist to disk self._persist(record_t, mode="a") + today = datetime.today().date() + fmt = "%H:%M:%S" if log_dt.date() == today else "%Y-%m-%d %H:%M:%S" + start_stop = "started" if type_ == wc.TOKEN_START else "stopped" + category_and_id = ( + "Session" if category == wc.TOKEN_SESSION else "Task " + (identifier or "") + ) + msg = "{} {} at {}".format(category_and_id, start_stop, log_dt.strftime(fmt),) + std_logger.info(msg) + # Because we allow for time offsets sorting is not guaranteed at this point. # Update sorting of values in-memory. self._log_df = self._log_df.sort_values(by=[wc.COL_LOG_DATETIME])