diff options
-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)) |