diff --git a/src/fime/data.py b/src/fime/data.py index 70d4b14..4c8144d 100644 --- a/src/fime/data.py +++ b/src/fime/data.py @@ -144,9 +144,7 @@ class Tasks: class LogCommentsData: - _log_key = "log" - _comments_key = "comments" - _init_day = {_log_key: [], _comments_key: {}} + _init_day = {"log": [], "comments": {}} # Data.__setitem__ only gets triggered, when there is a direct assignment (in contrast to assignment down the tree) # this necessitates reassigning the whole month, when triggering a save is intended @@ -160,12 +158,11 @@ class LogCommentsData: return if month_str not in self._data: return - day_str = pdate.strftime("%d") for day, log in self._data[month_str].items(): if type(log) is list: self._data[month_str][day] = { - self._log_key: log, - self._comments_key: {} + "log": log, + "comments": {} } self._converted.add(month_str) @@ -177,7 +174,7 @@ class LogCommentsData: return [] if day_str not in self._data[month_str]: return [] - log_data = self._data[month_str][day_str][self._log_key] + log_data = self._data[month_str][day_str]["log"] ret = [] for entry in log_data: tstr, b64str = entry.split() @@ -186,10 +183,7 @@ class LogCommentsData: ret.append((start_time, task)) return ret - def set_log(self, log: List[Tuple[datetime, str]]): - if not log: - return - pdate = log[0][0].date() + def set_log(self, pdate: date, log: List[Tuple[datetime, str]]): self._ensure_format(pdate) encoded = [] for entry in log: @@ -199,8 +193,10 @@ class LogCommentsData: month_str = pdate.strftime("%Y-%m") day_str = pdate.strftime("%d") month = self._data.setdefault(month_str, {}) - month.setdefault(day_str, self._init_day)[self._log_key] = encoded - self._data[month_str] = month + month.setdefault(day_str, self._init_day)["log"] = encoded + # trigger save if necessary + if self._data[month_str] != month: + self._data[month_str] = month def add_log_entry(self, task: str): now = datetime.now() @@ -211,9 +207,30 @@ class LogCommentsData: month_str = now.strftime("%Y-%m") day_str = now.strftime("%d") month = self._data.setdefault(month_str, {}) - month.setdefault(day_str, self._init_day)[self._log_key].append(encoded) + month.setdefault(day_str, self._init_day)["log"].append(encoded) self._data[month_str] = month + def get_prev_next_avail(self, pdate: date) -> Tuple[date, date]: + prev = None + next = None + for i in range(1, 32): + new_date = pdate - timedelta(days=i) + if new_date.strftime("%Y-%m") not in self._data: + break + if new_date.strftime("%d") in self._data[new_date.strftime("%Y-%m")]: + prev = new_date + break + for i in range(1, 32): + new_date = pdate + timedelta(days=i) + if new_date > date.today(): + break + if new_date.strftime("%Y-%m") not in self._data: + break + if new_date.strftime("%d") in self._data[new_date.strftime("%Y-%m")]: + next = new_date + break + return prev, next + def get_comments(self, pdate: date) -> Dict[str, str]: self._ensure_format(pdate) month_str = pdate.strftime("%Y-%m") @@ -222,7 +239,7 @@ class LogCommentsData: return dict() if day_str not in self._data[month_str]: return dict() - comment_data = self._data[month_str][day_str][self._comments_key] + comment_data = self._data[month_str][day_str]["comments"] ret = dict() for k, v in comment_data: k_dec = base64.b64decode(k.encode("utf-8")).decode("utf-8") @@ -240,8 +257,10 @@ class LogCommentsData: month_str = pdate.strftime("%Y-%m") day_str = pdate.strftime("%d") month = self._data.setdefault(month_str, {}) - month.setdefault(day_str, self._init_day)[self._comments_key] = encoded - self._data[month_str] = month + month.setdefault(day_str, self._init_day)["comments"] = encoded + # trigger save if necessary + if self._data[month_str] != month: + self._data[month_str] = month class Log: @@ -260,114 +279,94 @@ class Log: log = self._data.get_log(date.today()) if not log: return None - if log[-1] == "End": + if log[-1][1] == "End": del log[-1] - self._data.set_log(log) + self._data.set_log(date.today(), log) if not log: return None - return log[-1] + return log[-1][1] def report(self): - return Report(self._data, date.today()) + return Report(self._data) def worklog(self): return Worklog(self._data) -class Worklog: - def __init__(self, data): - self._data = data +def summary(lcd: LogCommentsData, pdate: date) -> Tuple[Dict[str, timedelta], timedelta]: + log = lcd.get_log(pdate) + if pdate == date.today(): + log.append((datetime.now(), "End")) + tasks_sums = {} + total_sum = timedelta() + for i, le in enumerate(log): + start_time, task = le + if i < len(log) - 1: + end_time = log[i+1][0] + duration = end_time - start_time + if task != "Pause": + task_sum = tasks_sums.setdefault(task, timedelta()) + task_sum += duration + tasks_sums[task] = task_sum + total_sum += duration + return tasks_sums, total_sum + + +def duration_to_str(duration: timedelta) -> str: + dhours, rem = divmod(duration.seconds, 3600) + dmins, _ = divmod(rem, 60) + return f"{dhours:02d}:{dmins:02d}" class Report: - def __init__(self, data, pdate): + def __init__(self, data: LogCommentsData): self._data = data - self._date = pdate - self._sum_len = 0 + self._date = date.today() + self._not_log_len = 0 self._prev = None self._next = None self._update_prev_next() - def report(self): - tmp = [] - if self._date.strftime("%Y-%m") in self._data \ - and self._date.strftime("%d") in self._data[self._date.strftime("%Y-%m")]: - for e in self._data[self._date.strftime("%Y-%m")][self._date.strftime("%d")]: - tstr, b64str = e.split() - task = base64.b64decode(b64str.encode("utf-8")).decode("utf-8") - start_time = datetime.combine(self._date, datetime.strptime(tstr, "%H:%M").time()) - tmp.append((task, start_time)) + def report(self) -> Tuple[List[List[str]], int]: + log = self._data.get_log(self._date) if self._date == date.today(): - tmp.append(("End", datetime.now())) - + log.append((datetime.now(), "End")) ret = [] - tasks_sums = {} - total_sum = timedelta() - for i, t in enumerate(tmp): - task, start_time = t - if i < len(tmp) - 1: - end_time = tmp[i+1][1] + for i, t in enumerate(log): + start_time, task = t + if i < len(log) - 1: + end_time = log[i+1][0] duration = end_time - start_time - if task != "Pause": - task_sum = tasks_sums.setdefault(task, timedelta()) - task_sum += duration - tasks_sums[task] = task_sum - total_sum += duration - dhours, rem = divmod(duration.seconds, 3600) - dmins, _ = divmod(rem, 60) - ret.append([task, start_time.strftime("%H:%M"), f"{dhours:02d}:{dmins:02d}"]) + ret.append([task, start_time.strftime("%H:%M"), duration_to_str(duration)]) else: ret.append([task, start_time.strftime("%H:%M"), ""]) ret.append(["", "", ""]) ret.append(["", "Sums", ""]) - for k, v in tasks_sums.items(): - dhours, rem = divmod(v.seconds, 3600) - dmins, _ = divmod(rem, 60) - ret.append([k, "", f"{dhours:02d}:{dmins:02d}"]) - dhours, rem = divmod(total_sum.seconds, 3600) - dmins, _ = divmod(rem, 60) - ret.append(["Total sum", "", f"{dhours:02d}:{dmins:02d}"]) - self._sum_len = 3 + len(tasks_sums) + + tasks_summary, total_sum = summary(self._data, self._date) + for task, duration in tasks_summary.items(): + ret.append([task, "", duration_to_str(duration)]) + ret.append(["Total sum", "", duration_to_str(total_sum)]) + self._not_log_len = 3 + len(tasks_summary) if self._date == date.today(): - self._sum_len += 1 - return ret, len(ret) - (4 + len(tasks_sums)) + self._not_log_len += 1 + editable_len = len(ret) - (4 + len(tasks_summary)) + + return ret, editable_len def save(self, report): - report = report[:-self._sum_len] + report = report[:-self._not_log_len] if not report: return - save_list = [] - for tstr, ttime, _ in report: - b64str = base64.b64encode(tstr.encode("utf-8")).decode("utf-8") - save_string = f"{ttime} {b64str}" - save_list.append(save_string) - # month dance necessary to trigger Data.__setitem__ - month = self._data[self._date.strftime("%Y-%m")] - if month[self._date.strftime("%d")] == save_list: # no changes - return - month[self._date.strftime("%d")] = save_list - self._data[self._date.strftime("%Y-%m")] = month + report = list(map( + lambda x: (datetime.combine(self._date, datetime.strptime(x[1], "%H:%M").time()), x[0]), + report + )) + self._data.set_log(self._date, report) def _update_prev_next(self): - self._prev = None - self._next = None - for i in range(1, 32): - new_date = self._date - timedelta(days=i) - if new_date.strftime("%Y-%m") not in self._data: - break - if new_date.strftime("%d") in self._data[new_date.strftime("%Y-%m")]: - self._prev = new_date - break - for i in range(1, 32): - new_date = self._date + timedelta(days=i) - if new_date > date.today(): - break - if new_date.strftime("%Y-%m") not in self._data: - break - if new_date.strftime("%d") in self._data[new_date.strftime("%Y-%m")]: - self._next = new_date - break + self._prev, self._next = self._data.get_prev_next_avail(self._date) def prev_next_avail(self): return self._prev is not None, self._next is not None @@ -382,3 +381,8 @@ class Report: def date(self): return self._date.strftime("%Y-%m-%d") + + +class Worklog: + def __init__(self, data: LogCommentsData): + self._data = data diff --git a/src/fime/main.py b/src/fime/main.py index fc3e123..4c28727 100755 --- a/src/fime/main.py +++ b/src/fime/main.py @@ -14,7 +14,7 @@ except ImportError: # noinspection PyUnresolvedReferences import fime.icons -from fime.data import Tasks, Log, Data +from fime.data import Tasks, Log, Data, LogCommentsData from fime.exceptions import FimeException from fime.import_task import ImportTask from fime.report import Report @@ -29,7 +29,8 @@ class App: data = Data() self.tasks = Tasks(data) - self.log = Log(data) + lcd = LogCommentsData(data) + self.log = Log(lcd) self._active_task = self.log.last_log() or "Nothing" self.config = Config()