diff options
Diffstat (limited to 'overlays/worktime')
| -rw-r--r-- | overlays/worktime/shell.nix | 5 | ||||
| -rwxr-xr-x | overlays/worktime/worktime.py | 59 |
2 files changed, 55 insertions, 9 deletions
diff --git a/overlays/worktime/shell.nix b/overlays/worktime/shell.nix new file mode 100644 index 00000000..18d2a68a --- /dev/null +++ b/overlays/worktime/shell.nix | |||
| @@ -0,0 +1,5 @@ | |||
| 1 | { pkgs ? import <nixpkgs> {} }: | ||
| 2 | |||
| 3 | pkgs.mkShell { | ||
| 4 | buildInputs = [(pkgs.python310.withPackages (ps: with ps; [pyxdg python-dateutil uritools requests configparser tabulate]))]; | ||
| 5 | } | ||
diff --git a/overlays/worktime/worktime.py b/overlays/worktime/worktime.py index 1fc00061..310fa084 100755 --- a/overlays/worktime/worktime.py +++ b/overlays/worktime/worktime.py | |||
| @@ -27,6 +27,9 @@ from sys import stderr | |||
| 27 | 27 | ||
| 28 | from tabulate import tabulate | 28 | from tabulate import tabulate |
| 29 | 29 | ||
| 30 | from itertools import groupby | ||
| 31 | from functools import cache | ||
| 32 | |||
| 30 | class TogglAPISection(Enum): | 33 | class TogglAPISection(Enum): |
| 31 | TOGGL = '/api/v8' | 34 | TOGGL = '/api/v8' |
| 32 | REPORTS = '/reports/api/v2' | 35 | REPORTS = '/reports/api/v2' |
| @@ -118,8 +121,10 @@ class Worktime(object): | |||
| 118 | leave_days = set() | 121 | leave_days = set() |
| 119 | leave_budget = dict() | 122 | leave_budget = dict() |
| 120 | time_per_day = None | 123 | time_per_day = None |
| 124 | workdays = None | ||
| 121 | 125 | ||
| 122 | @staticmethod | 126 | @staticmethod |
| 127 | @cache | ||
| 123 | def holidays(year): | 128 | def holidays(year): |
| 124 | holidays = dict() | 129 | holidays = dict() |
| 125 | 130 | ||
| @@ -149,6 +154,13 @@ class Worktime(object): | |||
| 149 | config.read(f"{config_dir}/worktime.ini") | 154 | config.read(f"{config_dir}/worktime.ini") |
| 150 | return config | 155 | return config |
| 151 | 156 | ||
| 157 | def ordinal_workday(self, date): | ||
| 158 | start_date = datetime(date.year, 1, 1, tzinfo=tzlocal()).date() | ||
| 159 | return len([1 for offset in range(0, (date - start_date).days + 1) if self.would_be_workday(start_date + timedelta(days = offset))]) | ||
| 160 | |||
| 161 | def would_be_workday(self, date): | ||
| 162 | return date.isoweekday() in self.workdays and date not in set(day for (day, val) in Worktime.holidays(date.year).items() if val >= 1) | ||
| 163 | |||
| 152 | def __init__(self, start_datetime=None, end_datetime=None, now=None, include_running=True, force_day_to_work=True, **kwargs): | 164 | def __init__(self, start_datetime=None, end_datetime=None, now=None, include_running=True, force_day_to_work=True, **kwargs): |
| 153 | self.include_running = include_running | 165 | self.include_running = include_running |
| 154 | self.force_day_to_work = force_day_to_work | 166 | self.force_day_to_work = force_day_to_work |
| @@ -178,8 +190,8 @@ class Worktime(object): | |||
| 178 | 190 | ||
| 179 | 191 | ||
| 180 | hours_per_week = float(config.get('WORKTIME', 'HoursPerWeek', fallback=40)) | 192 | hours_per_week = float(config.get('WORKTIME', 'HoursPerWeek', fallback=40)) |
| 181 | workdays = set([int(d.strip()) for d in config.get('WORKTIME', 'Workdays', fallback='1,2,3,4,5').split(',')]) | 193 | self.workdays = set([int(d.strip()) for d in config.get('WORKTIME', 'Workdays', fallback='1,2,3,4,5').split(',')]) |
| 182 | self.time_per_day = timedelta(hours = hours_per_week) / len(workdays) | 194 | self.time_per_day = timedelta(hours = hours_per_week) / len(self.workdays) |
| 183 | 195 | ||
| 184 | holidays = dict() | 196 | holidays = dict() |
| 185 | 197 | ||
| @@ -237,7 +249,7 @@ class Worktime(object): | |||
| 237 | if end_date.date() < day or day < start_date.date(): | 249 | if end_date.date() < day or day < start_date.date(): |
| 238 | continue | 250 | continue |
| 239 | 251 | ||
| 240 | if excused_kind == 'leave' and not (day in holidays and holidays[day] >= self.time_per_day) and day.isoweekday() in workdays: | 252 | if excused_kind == 'leave' and self.would_be_workday(day): |
| 241 | self.leave_days.add(day) | 253 | self.leave_days.add(day) |
| 242 | holidays[day] = time | 254 | holidays[day] = time |
| 243 | except IOError as e: | 255 | except IOError as e: |
| @@ -256,7 +268,7 @@ class Worktime(object): | |||
| 256 | if stripped_line: | 268 | if stripped_line: |
| 257 | [hours, datestr] = stripped_line.split(' ') | 269 | [hours, datestr] = stripped_line.split(' ') |
| 258 | constr = datestr.split(',') | 270 | constr = datestr.split(',') |
| 259 | for d in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1 + int(timedelta(hours = float(hours)).total_seconds() / 60 * (7 / len(workdays)) * 2))]: | 271 | for d in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1 + int(timedelta(hours = float(hours)).total_seconds() / 60 * (7 / len(self.workdays)) * 2))]: |
| 260 | for c in constr: | 272 | for c in constr: |
| 261 | if c in calendar.day_abbr: | 273 | if c in calendar.day_abbr: |
| 262 | if not d.strftime('%a') == c: break | 274 | if not d.strftime('%a') == c: break |
| @@ -283,7 +295,7 @@ class Worktime(object): | |||
| 283 | end_day = max(end_day, max(list(pull_forward))) | 295 | end_day = max(end_day, max(list(pull_forward))) |
| 284 | 296 | ||
| 285 | for day in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1)]: | 297 | for day in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1)]: |
| 286 | if day.isoweekday() in workdays: | 298 | if day.isoweekday() in self.workdays: |
| 287 | time_to_work = self.time_per_day | 299 | time_to_work = self.time_per_day |
| 288 | if day in holidays.keys(): | 300 | if day in holidays.keys(): |
| 289 | time_to_work -= holidays[day] | 301 | time_to_work -= holidays[day] |
| @@ -467,7 +479,8 @@ def holidays(year, **args): | |||
| 467 | print(tabulate(table_data, tablefmt="plain")) | 479 | print(tabulate(table_data, tablefmt="plain")) |
| 468 | 480 | ||
| 469 | def leave(year, table, **args): | 481 | def leave(year, table, **args): |
| 470 | worktime = Worktime(**dict(**args, end_datetime = datetime(year = year + 1, month = 1, day = 1, tzinfo=tzlocal()) - timedelta(microseconds=1))) | 482 | def_year = datetime.now(tzlocal()).year |
| 483 | worktime = Worktime(**dict(**args, end_datetime = datetime(year = (year if year else def_year) + 1, month = 1, day = 1, tzinfo=tzlocal()) - timedelta(microseconds=1))) | ||
| 471 | config = Worktime.config() | 484 | config = Worktime.config() |
| 472 | date_format = config.get('WORKTIME', 'DateFormat', fallback='%Y-%m-%d') | 485 | date_format = config.get('WORKTIME', 'DateFormat', fallback='%Y-%m-%d') |
| 473 | leave_expires = config.get('WORKTIME', 'LeaveExpires', fallback=None) | 486 | leave_expires = config.get('WORKTIME', 'LeaveExpires', fallback=None) |
| @@ -475,6 +488,7 @@ def leave(year, table, **args): | |||
| 475 | leave_expires = datetime.strptime(leave_expires, '%m-%d').date() | 488 | leave_expires = datetime.strptime(leave_expires, '%m-%d').date() |
| 476 | 489 | ||
| 477 | leave_budget = deepcopy(worktime.leave_budget) | 490 | leave_budget = deepcopy(worktime.leave_budget) |
| 491 | year_leave_budget = deepcopy(worktime.leave_budget) if year else None | ||
| 478 | years = sorted(leave_budget.keys()) | 492 | years = sorted(leave_budget.keys()) |
| 479 | for day in sorted(worktime.leave_days): | 493 | for day in sorted(worktime.leave_days): |
| 480 | for iyear in years: | 494 | for iyear in years: |
| @@ -484,18 +498,45 @@ def leave(year, table, **args): | |||
| 484 | continue | 498 | continue |
| 485 | 499 | ||
| 486 | leave_budget[iyear] -= 1 | 500 | leave_budget[iyear] -= 1 |
| 501 | if year_leave_budget and day.year < year: | ||
| 502 | year_leave_budget[iyear] -= 1 | ||
| 487 | break | 503 | break |
| 488 | else: | 504 | else: |
| 489 | print(f'Unaccounted leave: {day}', file=stderr) | 505 | print(f'Unaccounted leave: {day}', file=stderr) |
| 490 | 506 | ||
| 491 | if table: | 507 | if table and year: |
| 508 | table_data = [] | ||
| 509 | leave_days = sorted([day for day in worktime.leave_days if day.year == year and worktime.would_be_workday(day)]) | ||
| 510 | |||
| 511 | count = 0 | ||
| 512 | for _, group in groupby(enumerate(leave_days), lambda kv: kv[0] - worktime.ordinal_workday(kv[1])): | ||
| 513 | group = list(map(lambda kv: kv[1], group)) | ||
| 514 | |||
| 515 | for day in group: | ||
| 516 | for iyear in years: | ||
| 517 | if day > leave_expires.replace(year = iyear + 1): | ||
| 518 | continue | ||
| 519 | if year_leave_budget[iyear] <= 0: | ||
| 520 | continue | ||
| 521 | |||
| 522 | year_leave_budget[iyear] -= 1 | ||
| 523 | break | ||
| 524 | |||
| 525 | next_count = count + len(group) | ||
| 526 | if len(group) > 1: | ||
| 527 | table_data.append([count, group[0].strftime('%m-%d') + '--' + group[-1].strftime('%m-%d'), len(group), sum(year_leave_budget.values())]) | ||
| 528 | else: | ||
| 529 | table_data.append([count, group[0].strftime('%m-%d'), len(group), sum(year_leave_budget.values())]) | ||
| 530 | count = next_count | ||
| 531 | print(tabulate(table_data, tablefmt="plain")) | ||
| 532 | elif table: | ||
| 492 | table_data = [] | 533 | table_data = [] |
| 493 | for year, days in leave_budget.items(): | 534 | for year, days in leave_budget.items(): |
| 494 | leave_days = sorted([day for day in worktime.leave_days if day.year == year]) | 535 | leave_days = sorted([day for day in worktime.leave_days if day.year == year]) |
| 495 | table_data += [[year, days, ','.join(map(lambda d: d.strftime('%m-%d'), leave_days))]] | 536 | table_data += [[year, days, ','.join(map(lambda d: d.strftime('%m-%d'), leave_days))]] |
| 496 | print(tabulate(table_data, tablefmt="plain")) | 537 | print(tabulate(table_data, tablefmt="plain")) |
| 497 | else: | 538 | else: |
| 498 | print(leave_budget[year]) | 539 | print(leave_budget[year if year else def_year]) |
| 499 | 540 | ||
| 500 | def main(): | 541 | def main(): |
| 501 | parser = argparse.ArgumentParser(prog = "worktime", description = 'Track worktime using toggl API') | 542 | parser = argparse.ArgumentParser(prog = "worktime", description = 'Track worktime using toggl API') |
| @@ -513,7 +554,7 @@ def main(): | |||
| 513 | holidays_parser.add_argument('year', metavar = 'YEAR', type = int, help = 'Year to evaluate holidays for (default: current year)', default = datetime.now(tzlocal()).year, nargs='?') | 554 | holidays_parser.add_argument('year', metavar = 'YEAR', type = int, help = 'Year to evaluate holidays for (default: current year)', default = datetime.now(tzlocal()).year, nargs='?') |
| 514 | holidays_parser.set_defaults(cmd = holidays) | 555 | holidays_parser.set_defaults(cmd = holidays) |
| 515 | leave_parser = subparsers.add_parser('leave') | 556 | leave_parser = subparsers.add_parser('leave') |
| 516 | leave_parser.add_argument('year', metavar = 'YEAR', type = int, help = 'Year to evaluate leave days for (default: current year)', default = datetime.now(tzlocal()).year, nargs='?') | 557 | leave_parser.add_argument('year', metavar = 'YEAR', type = int, help = 'Year to evaluate leave days for (default: current year)', default = None, nargs='?') |
| 517 | leave_parser.add_argument('--table', action = 'store_true') | 558 | leave_parser.add_argument('--table', action = 'store_true') |
| 518 | leave_parser.set_defaults(cmd = leave) | 559 | leave_parser.set_defaults(cmd = leave) |
| 519 | args = parser.parse_args() | 560 | args = parser.parse_args() |
