summaryrefslogtreecommitdiff
path: root/overlays/worktime/worktime.py
diff options
context:
space:
mode:
Diffstat (limited to 'overlays/worktime/worktime.py')
-rwxr-xr-xoverlays/worktime/worktime.py114
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
15from enum import Enum 15from enum import Enum
16 16
17from math import (copysign, ceil) 17from math import (copysign, ceil, floor)
18 18
19import calendar 19import calendar
20 20
@@ -23,6 +23,7 @@ import argparse
23from copy import deepcopy 23from copy import deepcopy
24 24
25import sys 25import sys
26from sys import stderr
26 27
27from tabulate import tabulate 28from 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
398def holidays(now, **args): 443def 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
455def 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
411def main(): 486def 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))