summaryrefslogtreecommitdiff
path: root/overlays
diff options
context:
space:
mode:
Diffstat (limited to 'overlays')
-rw-r--r--overlays/worktime/shell.nix5
-rwxr-xr-xoverlays/worktime/worktime.py59
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
3pkgs.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
28from tabulate import tabulate 28from tabulate import tabulate
29 29
30from itertools import groupby
31from functools import cache
32
30class TogglAPISection(Enum): 33class 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
469def leave(year, table, **args): 481def 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
500def main(): 541def 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()