diff options
Diffstat (limited to 'overlays')
| -rwxr-xr-x | overlays/worktime/worktime.py | 114 |
1 files changed, 97 insertions, 17 deletions
diff --git a/overlays/worktime/worktime.py b/overlays/worktime/worktime.py index 678c0d69..9cfc6cd4 100755 --- a/overlays/worktime/worktime.py +++ b/overlays/worktime/worktime.py | |||
| @@ -14,7 +14,7 @@ from dateutil.parser import isoparse | |||
| 14 | 14 | ||
| 15 | from enum import Enum | 15 | from enum import Enum |
| 16 | 16 | ||
| 17 | from math import (copysign, ceil) | 17 | from math import (copysign, ceil, floor) |
| 18 | 18 | ||
| 19 | import calendar | 19 | import calendar |
| 20 | 20 | ||
| @@ -23,6 +23,7 @@ import argparse | |||
| 23 | from copy import deepcopy | 23 | from copy import deepcopy |
| 24 | 24 | ||
| 25 | import sys | 25 | import sys |
| 26 | from sys import stderr | ||
| 26 | 27 | ||
| 27 | from tabulate import tabulate | 28 | from tabulate import tabulate |
| 28 | 29 | ||
| @@ -114,6 +115,8 @@ class Worktime(object): | |||
| 114 | include_running = True | 115 | include_running = True |
| 115 | time_to_work = None | 116 | time_to_work = None |
| 116 | force_day_to_work = True | 117 | force_day_to_work = True |
| 118 | leave_days = set() | ||
| 119 | leave_budget = dict() | ||
| 117 | 120 | ||
| 118 | @staticmethod | 121 | @staticmethod |
| 119 | def holidays(year): | 122 | def holidays(year): |
| @@ -179,25 +182,67 @@ class Worktime(object): | |||
| 179 | 182 | ||
| 180 | holidays = dict() | 183 | holidays = dict() |
| 181 | 184 | ||
| 185 | leave_per_year = int(config.get('WORKTIME', 'LeavePerYear', fallback=30)) | ||
| 182 | for year in range(start_date.year, end_date.year + 1): | 186 | for year in range(start_date.year, end_date.year + 1): |
| 183 | holidays |= {k: v * time_per_day for k, v in Worktime.holidays(year).items()} | 187 | holidays |= {k: v * time_per_day for k, v in Worktime.holidays(year).items()} |
| 188 | leave_frac = 1 | ||
| 189 | if date(year, 1, 1) < start_date.date(): | ||
| 190 | leave_frac = (date(year + 1, 1, 1) - start_date.date()) / (date(year + 1, 1, 1) - date(year, 1, 1)) | ||
| 191 | self.leave_budget |= {year: floor(leave_per_year * leave_frac)} | ||
| 184 | 192 | ||
| 185 | try: | 193 | try: |
| 186 | with open(f"{config_dir}/excused", 'r') as excused: | 194 | with open(f"{config_dir}/reset-leave", 'r') as excused: |
| 187 | for line in excused: | 195 | for line in excused: |
| 188 | stripped_line = line.strip() | 196 | stripped_line = line.strip() |
| 189 | if stripped_line: | 197 | if stripped_line: |
| 190 | splitLine = stripped_line.split(' ') | 198 | [datestr, count] = stripped_line.split(' ') |
| 191 | if len(splitLine) == 2: | 199 | day = datetime.strptime(datestr, date_format).replace(tzinfo=tzlocal()).date() |
| 192 | [hours, date] = splitLine | 200 | if day != start_date.date(): |
| 193 | day = datetime.strptime(date, date_format).replace(tzinfo=tzlocal()).date() | 201 | continue |
| 194 | holidays[day] = timedelta(hours = float(hours)) | 202 | |
| 195 | else: | 203 | self.leave_budget[day.year] = (self.leave_budget[day.year] if day.year in self.leave_budget else 0) + int(count) |
| 196 | holidays[datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()).date()] = time_per_day | ||
| 197 | except IOError as e: | 204 | except IOError as e: |
| 198 | if e.errno != 2: | 205 | if e.errno != 2: |
| 199 | raise e | 206 | raise e |
| 200 | 207 | ||
| 208 | |||
| 209 | for excused_kind in {'excused', 'leave'}: | ||
| 210 | try: | ||
| 211 | with open(f"{config_dir}/{excused_kind}", 'r') as excused: | ||
| 212 | for line in excused: | ||
| 213 | stripped_line = line.strip() | ||
| 214 | if stripped_line: | ||
| 215 | splitLine = stripped_line.split(' ') | ||
| 216 | fromDay = toDay = None | ||
| 217 | def parse_datestr(datestr): | ||
| 218 | nonlocal fromDay, toDay | ||
| 219 | def parse_single(singlestr): | ||
| 220 | return datetime.strptime(singlestr, date_format).replace(tzinfo=tzlocal()).date() | ||
| 221 | if '--' in datestr: | ||
| 222 | [fromDay,toDay] = datestr.split('--') | ||
| 223 | fromDay = parse_single(fromDay) | ||
| 224 | toDay = parse_single(toDay) | ||
| 225 | else: | ||
| 226 | fromDay = toDay = parse_single(datestr) | ||
| 227 | time = time_per_day | ||
| 228 | if len(splitLine) == 2: | ||
| 229 | [hours, datestr] = splitLine | ||
| 230 | time = timedelta(hours = float(hours)) | ||
| 231 | parse_datestr(datestr) | ||
| 232 | else: | ||
| 233 | parse_datestr(stripped_line) | ||
| 234 | |||
| 235 | for day in [fromDay + timedelta(days = x) for x in range(0, (toDay - fromDay).days + 1)]: | ||
| 236 | if end_date.date() < day or day < start_date.date(): | ||
| 237 | continue | ||
| 238 | |||
| 239 | if excused_kind == 'leave' and not (day in holidays and holidays[day] >= time_per_day) and day.isoweekday() in workdays: | ||
| 240 | self.leave_days.add(day) | ||
| 241 | holidays[day] = time | ||
| 242 | except IOError as e: | ||
| 243 | if e.errno != 2: | ||
| 244 | raise e | ||
| 245 | |||
| 201 | pull_forward = dict() | 246 | pull_forward = dict() |
| 202 | 247 | ||
| 203 | start_day = start_date.date() | 248 | start_day = start_date.date() |
| @@ -208,8 +253,8 @@ class Worktime(object): | |||
| 208 | for line in excused: | 253 | for line in excused: |
| 209 | stripped_line = line.strip() | 254 | stripped_line = line.strip() |
| 210 | if stripped_line: | 255 | if stripped_line: |
| 211 | [hours, date] = stripped_line.split(' ') | 256 | [hours, datestr] = stripped_line.split(' ') |
| 212 | constr = date.split(',') | 257 | constr = datestr.split(',') |
| 213 | 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))]: | 258 | 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))]: |
| 214 | for c in constr: | 259 | for c in constr: |
| 215 | if c in calendar.day_abbr: | 260 | if c in calendar.day_abbr: |
| @@ -253,8 +298,8 @@ class Worktime(object): | |||
| 253 | if stripped_line: | 298 | if stripped_line: |
| 254 | splitLine = stripped_line.split(' ') | 299 | splitLine = stripped_line.split(' ') |
| 255 | if len(splitLine) == 2: | 300 | if len(splitLine) == 2: |
| 256 | [hours, date] = splitLine | 301 | [hours, datestr] = splitLine |
| 257 | day = datetime.strptime(date, date_format).replace(tzinfo=tzlocal()).date() | 302 | day = datetime.strptime(datestr, date_format).replace(tzinfo=tzlocal()).date() |
| 258 | extra_days_to_work[day] = timedelta(hours = float(hours)) | 303 | extra_days_to_work[day] = timedelta(hours = float(hours)) |
| 259 | else: | 304 | else: |
| 260 | extra_days_to_work[datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()).date()] = time_per_day | 305 | extra_days_to_work[datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()).date()] = time_per_day |
| @@ -395,19 +440,49 @@ def diff(now, **args): | |||
| 395 | 440 | ||
| 396 | print(now.time_to_work - then.time_to_work) | 441 | print(now.time_to_work - then.time_to_work) |
| 397 | 442 | ||
| 398 | def holidays(now, **args): | 443 | def holidays(year, **args): |
| 399 | config = Worktime.config() | 444 | config = Worktime.config() |
| 400 | date_format = config.get('WORKTIME', 'DateFormat', fallback='%Y-%m-%d') | 445 | date_format = config.get('WORKTIME', 'DateFormat', fallback='%Y-%m-%d') |
| 401 | 446 | ||
| 402 | table_data = [] | 447 | table_data = [] |
| 403 | 448 | ||
| 404 | holidays = Worktime.holidays(now.year) | 449 | holidays = Worktime.holidays(year) |
| 405 | for k, v in holidays.items(): | 450 | for k, v in holidays.items(): |
| 406 | kstr = k.strftime(date_format) | 451 | kstr = k.strftime(date_format) |
| 407 | |||
| 408 | table_data += [[kstr, v]] | 452 | table_data += [[kstr, v]] |
| 409 | print(tabulate(table_data, tablefmt="plain")) | 453 | print(tabulate(table_data, tablefmt="plain")) |
| 410 | 454 | ||
| 455 | def leave(year, table, **args): | ||
| 456 | worktime = Worktime(**dict(**args, end_datetime = datetime(year = year + 1, month = 1, day = 1, tzinfo=tzlocal()) - timedelta(microseconds=1))) | ||
| 457 | config = Worktime.config() | ||
| 458 | date_format = config.get('WORKTIME', 'DateFormat', fallback='%Y-%m-%d') | ||
| 459 | leave_expires = config.get('WORKTIME', 'LeaveExpires', fallback=None) | ||
| 460 | if leave_expires: | ||
| 461 | leave_expires = datetime.strptime(leave_expires, '%m-%d').date() | ||
| 462 | |||
| 463 | leave_budget = deepcopy(worktime.leave_budget) | ||
| 464 | years = sorted(leave_budget.keys()) | ||
| 465 | for day in sorted(worktime.leave_days): | ||
| 466 | for iyear in years: | ||
| 467 | if day > leave_expires.replace(year = iyear + 1): | ||
| 468 | continue | ||
| 469 | if leave_budget[iyear] <= 0: | ||
| 470 | continue | ||
| 471 | |||
| 472 | leave_budget[iyear] -= 1 | ||
| 473 | break | ||
| 474 | else: | ||
| 475 | print(f'Unaccounted leave: {day}', file=stderr) | ||
| 476 | |||
| 477 | if table: | ||
| 478 | table_data = [] | ||
| 479 | for year, days in leave_budget.items(): | ||
| 480 | leave_days = sorted([day for day in worktime.leave_days if day.year == year]) | ||
| 481 | table_data += [[year, days, ','.join(map(lambda d: d.strftime('%m-%d'), leave_days))]] | ||
| 482 | print(tabulate(table_data, tablefmt="plain")) | ||
| 483 | else: | ||
| 484 | print(leave_budget[year]) | ||
| 485 | |||
| 411 | def main(): | 486 | def main(): |
| 412 | parser = argparse.ArgumentParser(prog = "worktime", description = 'Track worktime using toggl API') | 487 | parser = argparse.ArgumentParser(prog = "worktime", description = 'Track worktime using toggl API') |
| 413 | parser.add_argument('--time', dest = 'now', metavar = 'TIME', type = lambda s: datetime.fromisoformat(s).replace(tzinfo=tzlocal()), help = 'Time to calculate status for (default: current time)', default = datetime.now(tzlocal())) | 488 | parser.add_argument('--time', dest = 'now', metavar = 'TIME', type = lambda s: datetime.fromisoformat(s).replace(tzinfo=tzlocal()), help = 'Time to calculate status for (default: current time)', default = datetime.now(tzlocal())) |
| @@ -421,7 +496,12 @@ def main(): | |||
| 421 | diff_parser = subparsers.add_parser('diff') | 496 | diff_parser = subparsers.add_parser('diff') |
| 422 | diff_parser.set_defaults(cmd = diff) | 497 | diff_parser.set_defaults(cmd = diff) |
| 423 | holidays_parser = subparsers.add_parser('holidays') | 498 | holidays_parser = subparsers.add_parser('holidays') |
| 499 | holidays_parser.add_argument('year', metavar = 'YEAR', type = int, help = 'Year to evaluate holidays for (default: current year)', default = datetime.now(tzlocal()).year, nargs='?') | ||
| 424 | holidays_parser.set_defaults(cmd = holidays) | 500 | holidays_parser.set_defaults(cmd = holidays) |
| 501 | leave_parser = subparsers.add_parser('leave') | ||
| 502 | 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='?') | ||
| 503 | leave_parser.add_argument('--table', action = 'store_true') | ||
| 504 | leave_parser.set_defaults(cmd = leave) | ||
| 425 | args = parser.parse_args() | 505 | args = parser.parse_args() |
| 426 | 506 | ||
| 427 | args.cmd(**vars(args)) | 507 | args.cmd(**vars(args)) |
