diff options
-rwxr-xr-x | overlays/worktime/worktime/__main__.py | 170 |
1 files changed, 134 insertions, 36 deletions
diff --git a/overlays/worktime/worktime/__main__.py b/overlays/worktime/worktime/__main__.py index 2dc9ed72..9bc7ac3b 100755 --- a/overlays/worktime/worktime/__main__.py +++ b/overlays/worktime/worktime/__main__.py | |||
@@ -26,12 +26,14 @@ from sys import stderr | |||
26 | from tabulate import tabulate | 26 | from tabulate import tabulate |
27 | 27 | ||
28 | from itertools import groupby | 28 | from itertools import groupby |
29 | from functools import cache | 29 | from functools import cache, partial |
30 | 30 | ||
31 | import backoff | 31 | import backoff |
32 | 32 | ||
33 | from pathlib import Path | 33 | from pathlib import Path |
34 | 34 | ||
35 | from collections import defaultdict | ||
36 | |||
35 | 37 | ||
36 | class TogglAPISection(Enum): | 38 | class TogglAPISection(Enum): |
37 | TOGGL = '/api/v8' | 39 | TOGGL = '/api/v8' |
@@ -168,7 +170,7 @@ class Worktime(object): | |||
168 | running_entry = None | 170 | running_entry = None |
169 | now = datetime.now(tzlocal()) | 171 | now = datetime.now(tzlocal()) |
170 | time_pulled_forward = timedelta() | 172 | time_pulled_forward = timedelta() |
171 | is_workday = False | 173 | now_is_workday = False |
172 | include_running = True | 174 | include_running = True |
173 | time_to_work = None | 175 | time_to_work = None |
174 | force_day_to_work = True | 176 | force_day_to_work = True |
@@ -213,6 +215,9 @@ class Worktime(object): | |||
213 | def would_be_workday(self, date): | 215 | def would_be_workday(self, date): |
214 | return date.isoweekday() in self.workdays and date not in set(day for (day, val) in Worktime.holidays(date.year).items() if val >= 1) | 216 | return date.isoweekday() in self.workdays and date not in set(day for (day, val) in Worktime.holidays(date.year).items() if val >= 1) |
215 | 217 | ||
218 | def is_workday(self, date, extra=True): | ||
219 | return date in self.days_to_work or (extra and date in self.extra_days_to_work) | ||
220 | |||
216 | def __init__(self, start_datetime=None, end_datetime=None, now=None, include_running=True, force_day_to_work=True, **kwargs): | 221 | def __init__(self, start_datetime=None, end_datetime=None, now=None, include_running=True, force_day_to_work=True, **kwargs): |
217 | self.include_running = include_running | 222 | self.include_running = include_running |
218 | self.force_day_to_work = force_day_to_work | 223 | self.force_day_to_work = force_day_to_work |
@@ -229,8 +234,8 @@ class Worktime(object): | |||
229 | ) | 234 | ) |
230 | date_format = config.get("WORKTIME", {}).get("DateFormat", '%Y-%m-%d') | 235 | date_format = config.get("WORKTIME", {}).get("DateFormat", '%Y-%m-%d') |
231 | 236 | ||
232 | start_date = start_datetime or datetime.strptime(config.get("WORKTIME", {}).get("StartDate"), date_format).replace(tzinfo=tzlocal()) | 237 | self.start_date = start_datetime or datetime.strptime(config.get("WORKTIME", {}).get("StartDate"), date_format).replace(tzinfo=tzlocal()) |
233 | end_date = end_datetime or self.now | 238 | self.end_date = end_datetime or self.now |
234 | 239 | ||
235 | try: | 240 | try: |
236 | with open(Path(config_dir) / "reset", 'r') as reset: | 241 | with open(Path(config_dir) / "reset", 'r') as reset: |
@@ -238,8 +243,8 @@ class Worktime(object): | |||
238 | stripped_line = line.strip() | 243 | stripped_line = line.strip() |
239 | reset_date = datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()) | 244 | reset_date = datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()) |
240 | 245 | ||
241 | if reset_date > start_date and reset_date <= end_date: | 246 | if reset_date > self.start_date and reset_date <= self.end_date: |
242 | start_date = reset_date | 247 | self.start_date = reset_date |
243 | except IOError as e: | 248 | except IOError as e: |
244 | if e.errno != 2: | 249 | if e.errno != 2: |
245 | raise e | 250 | raise e |
@@ -252,11 +257,11 @@ class Worktime(object): | |||
252 | holidays = dict() | 257 | holidays = dict() |
253 | 258 | ||
254 | leave_per_year = int(config.get("WORKTIME", {}).get("LeavePerYear", 30)) | 259 | leave_per_year = int(config.get("WORKTIME", {}).get("LeavePerYear", 30)) |
255 | for year in range(start_date.year, end_date.year + 1): | 260 | for year in range(self.start_date.year, self.end_date.year + 1): |
256 | holidays |= {k: v * self.time_per_day for k, v in Worktime.holidays(year).items()} | 261 | holidays |= {k: v * self.time_per_day for k, v in Worktime.holidays(year).items()} |
257 | leave_frac = 1 | 262 | leave_frac = 1 |
258 | if date(year, 1, 1) < start_date.date(): | 263 | if date(year, 1, 1) < self.start_date.date(): |
259 | leave_frac = (date(year + 1, 1, 1) - start_date.date()) / (date(year + 1, 1, 1) - date(year, 1, 1)) | 264 | leave_frac = (date(year + 1, 1, 1) - self.start_date.date()) / (date(year + 1, 1, 1) - date(year, 1, 1)) |
260 | self.leave_budget |= {year: floor(leave_per_year * leave_frac)} | 265 | self.leave_budget |= {year: floor(leave_per_year * leave_frac)} |
261 | 266 | ||
262 | try: | 267 | try: |
@@ -266,7 +271,7 @@ class Worktime(object): | |||
266 | if stripped_line: | 271 | if stripped_line: |
267 | [datestr, count] = stripped_line.split(' ') | 272 | [datestr, count] = stripped_line.split(' ') |
268 | day = datetime.strptime(datestr, date_format).replace(tzinfo=tzlocal()).date() | 273 | day = datetime.strptime(datestr, date_format).replace(tzinfo=tzlocal()).date() |
269 | if day != start_date.date(): | 274 | if day != self.start_date.date(): |
270 | continue | 275 | continue |
271 | 276 | ||
272 | self.leave_budget[day.year] = (self.leave_budget[day.year] if day.year in self.leave_budget else 0) + int(count) | 277 | self.leave_budget[day.year] = (self.leave_budget[day.year] if day.year in self.leave_budget else 0) + int(count) |
@@ -302,7 +307,7 @@ class Worktime(object): | |||
302 | parse_datestr(stripped_line) | 307 | parse_datestr(stripped_line) |
303 | 308 | ||
304 | for day in [fromDay + timedelta(days = x) for x in range(0, (toDay - fromDay).days + 1)]: | 309 | for day in [fromDay + timedelta(days = x) for x in range(0, (toDay - fromDay).days + 1)]: |
305 | if end_date.date() < day or day < start_date.date(): | 310 | if self.end_date.date() < day or day < self.start_date.date(): |
306 | continue | 311 | continue |
307 | 312 | ||
308 | if excused_kind == 'leave' and self.would_be_workday(day): | 313 | if excused_kind == 'leave' and self.would_be_workday(day): |
@@ -314,8 +319,8 @@ class Worktime(object): | |||
314 | 319 | ||
315 | pull_forward = dict() | 320 | pull_forward = dict() |
316 | 321 | ||
317 | start_day = start_date.date() | 322 | start_day = self.start_date.date() |
318 | end_day = end_date.date() | 323 | end_day = self.end_date.date() |
319 | 324 | ||
320 | try: | 325 | try: |
321 | with open(Path(config_dir) / "pull-forward", 'r') as excused: | 326 | with open(Path(config_dir) / "pull-forward", 'r') as excused: |
@@ -339,13 +344,13 @@ class Worktime(object): | |||
339 | else: | 344 | else: |
340 | if not d == datetime.strptime(c, date_format).replace(tzinfo=tzlocal()).date(): break | 345 | if not d == datetime.strptime(c, date_format).replace(tzinfo=tzlocal()).date(): break |
341 | else: | 346 | else: |
342 | if d >= end_date.date(): | 347 | if d >= self.end_date.date(): |
343 | pull_forward[d] = min(timedelta(hours = float(hours)), self.time_per_day - (holidays[d] if d in holidays else timedelta())) | 348 | pull_forward[d] = min(timedelta(hours = float(hours)), self.time_per_day - (holidays[d] if d in holidays else timedelta())) |
344 | except IOError as e: | 349 | except IOError as e: |
345 | if e.errno != 2: | 350 | if e.errno != 2: |
346 | raise e | 351 | raise e |
347 | 352 | ||
348 | days_to_work = dict() | 353 | self.days_to_work = dict() |
349 | 354 | ||
350 | if pull_forward: | 355 | if pull_forward: |
351 | end_day = max(end_day, max(list(pull_forward))) | 356 | end_day = max(end_day, max(list(pull_forward))) |
@@ -356,9 +361,9 @@ class Worktime(object): | |||
356 | if day in holidays.keys(): | 361 | if day in holidays.keys(): |
357 | time_to_work -= holidays[day] | 362 | time_to_work -= holidays[day] |
358 | if time_to_work > timedelta(): | 363 | if time_to_work > timedelta(): |
359 | days_to_work[day] = time_to_work | 364 | self.days_to_work[day] = time_to_work |
360 | 365 | ||
361 | extra_days_to_work = dict() | 366 | self.extra_days_to_work = dict() |
362 | 367 | ||
363 | try: | 368 | try: |
364 | with open(Path(config_dir) / "days-to-work", 'r') as extra_days_to_work_file: | 369 | with open(Path(config_dir) / "days-to-work", 'r') as extra_days_to_work_file: |
@@ -369,15 +374,15 @@ class Worktime(object): | |||
369 | if len(splitLine) == 2: | 374 | if len(splitLine) == 2: |
370 | [hours, datestr] = splitLine | 375 | [hours, datestr] = splitLine |
371 | day = datetime.strptime(datestr, date_format).replace(tzinfo=tzlocal()).date() | 376 | day = datetime.strptime(datestr, date_format).replace(tzinfo=tzlocal()).date() |
372 | extra_days_to_work[day] = timedelta(hours = float(hours)) | 377 | self.extra_days_to_work[day] = timedelta(hours = float(hours)) |
373 | else: | 378 | else: |
374 | extra_days_to_work[datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()).date()] = self.time_per_day | 379 | self.extra_days_to_work[datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()).date()] = self.time_per_day |
375 | except IOError as e: | 380 | except IOError as e: |
376 | if e.errno != 2: | 381 | if e.errno != 2: |
377 | raise e | 382 | raise e |
378 | 383 | ||
379 | 384 | ||
380 | self.is_workday = self.now.date() in days_to_work or self.now.date() in extra_days_to_work | 385 | self.now_is_workday = self.is_workday(self.now.date()) |
381 | 386 | ||
382 | self.time_worked = timedelta() | 387 | self.time_worked = timedelta() |
383 | 388 | ||
@@ -387,37 +392,37 @@ class Worktime(object): | |||
387 | if self.running_entry: | 392 | if self.running_entry: |
388 | self.time_worked += self.running_entry | 393 | self.time_worked += self.running_entry |
389 | 394 | ||
390 | if self.running_entry and self.include_running and self.force_day_to_work and not (self.now.date() in days_to_work or self.now.date() in extra_days_to_work): | 395 | if self.running_entry and self.include_running and self.force_day_to_work and not (self.now.date() in self.days_to_work or self.now.date() in self.extra_days_to_work): |
391 | extra_days_to_work[self.now.date()] = timedelta() | 396 | self.extra_days_to_work[self.now.date()] = timedelta() |
392 | 397 | ||
393 | self.time_to_work = sum([days_to_work[day] for day in days_to_work.keys() if day <= end_date.date()], timedelta()) | 398 | self.time_to_work = sum([self.days_to_work[day] for day in self.days_to_work.keys() if day <= self.end_date.date()], timedelta()) |
394 | for day in [d for d in list(pull_forward) if d > end_date.date()]: | 399 | for day in [d for d in list(pull_forward) if d > self.end_date.date()]: |
395 | days_forward = set([d for d in days_to_work.keys() if d >= end_date.date() and d < day and (not d in pull_forward or d == end_date.date())]) | 400 | days_forward = set([d for d in self.days_to_work.keys() if d >= self.end_date.date() and d < day and (not d in pull_forward or d == self.end_date.date())]) |
396 | extra_days_forward = set([d for d in extra_days_to_work.keys() if d >= end_date.date() and d < day and (not d in pull_forward or d == end_date.date())]) | 401 | extra_days_forward = set([d for d in self.extra_days_to_work.keys() if d >= self.end_date.date() and d < day and (not d in pull_forward or d == self.end_date.date())]) |
397 | days_forward = days_forward.union(extra_days_forward) | 402 | days_forward = days_forward.union(extra_days_forward) |
398 | 403 | ||
399 | extra_day_time_left = timedelta() | 404 | extra_day_time_left = timedelta() |
400 | for extra_day in extra_days_forward: | 405 | for extra_day in extra_days_forward: |
401 | day_time = max(timedelta(), self.time_per_day - extra_days_to_work[extra_day]) | 406 | day_time = max(timedelta(), self.time_per_day - self.extra_days_to_work[extra_day]) |
402 | extra_day_time_left += day_time | 407 | extra_day_time_left += day_time |
403 | extra_day_time = min(extra_day_time_left, pull_forward[day]) | 408 | extra_day_time = min(extra_day_time_left, pull_forward[day]) |
404 | time_forward = pull_forward[day] - extra_day_time | 409 | time_forward = pull_forward[day] - extra_day_time |
405 | if extra_day_time_left > timedelta(): | 410 | if extra_day_time_left > timedelta(): |
406 | for extra_day in extra_days_forward: | 411 | for extra_day in extra_days_forward: |
407 | day_time = max(timedelta(), self.time_per_day - extra_days_to_work[extra_day]) | 412 | day_time = max(timedelta(), self.time_per_day - self.extra_days_to_work[extra_day]) |
408 | extra_days_to_work[extra_day] += extra_day_time * (day_time / extra_day_time_left) | 413 | self.extra_days_to_work[extra_day] += extra_day_time * (day_time / extra_day_time_left) |
409 | 414 | ||
410 | hours_per_day_forward = time_forward / len(days_forward) if len(days_forward) > 0 else timedelta() | 415 | hours_per_day_forward = time_forward / len(days_forward) if len(days_forward) > 0 else timedelta() |
411 | days_forward.discard(end_date.date()) | 416 | days_forward.discard(self.end_date.date()) |
412 | 417 | ||
413 | self.time_pulled_forward += time_forward - hours_per_day_forward * len(days_forward) | 418 | self.time_pulled_forward += time_forward - hours_per_day_forward * len(days_forward) |
414 | 419 | ||
415 | if end_date.date() in extra_days_to_work: | 420 | if self.end_date.date() in self.extra_days_to_work: |
416 | self.time_pulled_forward += extra_days_to_work[end_date.date()] | 421 | self.time_pulled_forward += self.extra_days_to_work[self.end_date.date()] |
417 | 422 | ||
418 | self.time_to_work += self.time_pulled_forward | 423 | self.time_to_work += self.time_pulled_forward |
419 | 424 | ||
420 | self.time_worked += api.get_billable_hours(start_date, self.now, rounding = config.get("WORKTIME", {}).get("rounding", True)) | 425 | self.time_worked += api.get_billable_hours(self.start_date, self.now, rounding = config.get("WORKTIME", {}).get("rounding", True)) |
421 | 426 | ||
422 | def worktime(**args): | 427 | def worktime(**args): |
423 | worktime = Worktime(**args) | 428 | worktime = Worktime(**args) |
@@ -458,7 +463,7 @@ def worktime(**args): | |||
458 | return f"{indicator}{difference_string}" | 463 | return f"{indicator}{difference_string}" |
459 | else: | 464 | else: |
460 | difference_string = difference_string(total_minutes_difference * timedelta(minutes = 1)) | 465 | difference_string = difference_string(total_minutes_difference * timedelta(minutes = 1)) |
461 | if worktime.is_workday: | 466 | if worktime.now_is_workday: |
462 | return difference_string | 467 | return difference_string |
463 | else: | 468 | else: |
464 | return f"({difference_string})" | 469 | return f"({difference_string})" |
@@ -498,7 +503,7 @@ def time_worked(now, **args): | |||
498 | 503 | ||
499 | clockout_time = None | 504 | clockout_time = None |
500 | clockout_difference = None | 505 | clockout_difference = None |
501 | if then.is_workday or now.is_workday: | 506 | if then.now_is_workday or now.now_is_workday: |
502 | target_time = max(then.time_per_day, now.time_per_day) if then.time_per_day and now.time_per_day else (then.time_per_day if then.time_per_day else now.time_per_day); | 507 | target_time = max(then.time_per_day, now.time_per_day) if then.time_per_day and now.time_per_day else (then.time_per_day if then.time_per_day else now.time_per_day); |
503 | difference = target_time - worked | 508 | difference = target_time - worked |
504 | clockout_difference = 5 * ceil(difference / timedelta(minutes = 5)) | 509 | clockout_difference = 5 * ceil(difference / timedelta(minutes = 5)) |
@@ -543,6 +548,8 @@ def leave(year, table, **args): | |||
543 | if leave_expires: | 548 | if leave_expires: |
544 | leave_expires = datetime.strptime(leave_expires, '%m-%d').date() | 549 | leave_expires = datetime.strptime(leave_expires, '%m-%d').date() |
545 | 550 | ||
551 | days = [worktime.start_date.date() + timedelta(days = x) for x in range(0, (worktime.end_date.date() - worktime.start_date.date()).days + 1)] | ||
552 | |||
546 | leave_budget = deepcopy(worktime.leave_budget) | 553 | leave_budget = deepcopy(worktime.leave_budget) |
547 | year_leave_budget = deepcopy(worktime.leave_budget) if year else None | 554 | year_leave_budget = deepcopy(worktime.leave_budget) if year else None |
548 | years = sorted(leave_budget.keys()) | 555 | years = sorted(leave_budget.keys()) |
@@ -589,12 +596,99 @@ def leave(year, table, **args): | |||
589 | table_data = [] | 596 | table_data = [] |
590 | for year, days in leave_budget.items(): | 597 | for year, days in leave_budget.items(): |
591 | leave_days = sorted([day for day in worktime.leave_days if day.year == year]) | 598 | leave_days = sorted([day for day in worktime.leave_days if day.year == year]) |
592 | table_data += [[year, days, ','.join(map(lambda d: d.strftime('%m-%d'), leave_days))]] | 599 | would_be_workdays = [day for day in days if day.year == year and worktime.would_be_workday(day)] |
600 | table_data += [[year, days, f"{len(leave_days)}/{len(list(would_be_workdays))}", ','.join(map(lambda d: d.strftime('%m-%d'), leave_days))]] | ||
593 | print(tabulate(table_data, tablefmt="plain")) | 601 | print(tabulate(table_data, tablefmt="plain")) |
594 | else: | 602 | else: |
595 | print(leave_budget[year if year else def_year]) | 603 | print(leave_budget[year if year else def_year]) |
596 | 604 | ||
605 | def classification(classification_name, table, **args): | ||
606 | worktime = Worktime(**args) | ||
607 | config = Worktime.config() | ||
608 | date_format = config.get("WORKTIME", {}).get("DateFormat", '%Y-%m-%d') | ||
609 | config_dir = BaseDirectory.load_first_config('worktime') | ||
610 | days = [worktime.start_date.date() + timedelta(days = x) for x in range(0, (worktime.end_date.date() - worktime.start_date.date()).days + 1)] | ||
611 | |||
612 | year_classification = defaultdict(dict) | ||
613 | year_offset = defaultdict(lambda: 0) | ||
614 | |||
615 | extra_classified = dict() | ||
616 | classification_files = { | ||
617 | Path(config_dir) / classification_name: True, | ||
618 | Path(config_dir) / f"extra-{classification_name}": True, | ||
619 | Path(config_dir) / f"not-{classification_name}": False, | ||
620 | } | ||
621 | for path, val in classification_files.items(): | ||
622 | try: | ||
623 | with path.open('r') as fh: | ||
624 | for line in fh: | ||
625 | stripped_line = line.strip() | ||
626 | if stripped_line: | ||
627 | fromDay = toDay = None | ||
628 | def parse_single(singlestr): | ||
629 | return datetime.strptime(singlestr, date_format).replace(tzinfo=tzlocal()).date() | ||
630 | if '--' in stripped_line: | ||
631 | [fromDay,toDay] = stripped_line.split('--') | ||
632 | fromDay = parse_single(fromDay) | ||
633 | toDay = parse_single(toDay) | ||
634 | else: | ||
635 | fromDay = toDay = parse_single(stripped_line) | ||
636 | |||
637 | for day in [fromDay + timedelta(days = x) for x in range(0, (toDay - fromDay).days + 1)]: | ||
638 | extra_classified[day] = val | ||
639 | except IOError as e: | ||
640 | if e.errno != 2: | ||
641 | raise e | ||
642 | |||
643 | for day in days: | ||
644 | if not worktime.is_workday(day, extra=False): | ||
645 | continue | ||
646 | |||
647 | classification_days = set() | ||
648 | for default_classification in reversed(config.get("day-classification", {}).get(classification_name, {}).get("default", [])): | ||
649 | from_date = None | ||
650 | to_date = None | ||
651 | |||
652 | if "from" in default_classification: | ||
653 | from_date = datetime.strptime(default_classification["from"], date_format).replace(tzinfo=tzlocal()).date() | ||
654 | if day < from_date: | ||
655 | continue | ||
656 | |||
657 | if "to" in default_classification: | ||
658 | to_date = datetime.strptime(default_classification["to"], date_format).replace(tzinfo=tzlocal()).date() | ||
659 | if day >= to_date: | ||
660 | continue | ||
661 | |||
662 | classification_days = set([int(d.strip()) for d in default_classification.get("days", "").split(',') if d.strip()]) | ||
663 | |||
664 | default_classification = day.isoweekday() in classification_days | ||
665 | override = None | ||
666 | if day in extra_classified: | ||
667 | override = extra_classified[day] | ||
668 | if override != default_classification: | ||
669 | year_offset[day.year] += 1 if override else -1 | ||
670 | year_classification[day.year][day] = override if override is not None else default_classification | ||
671 | |||
672 | if not table: | ||
673 | print(sum(year_offset.values())) | ||
674 | else: | ||
675 | table_data = [] | ||
676 | for year in sorted(year_classification.keys()): | ||
677 | row_data = [year] | ||
678 | count_classified = len([1 for day, classified in year_classification[year].items() if classified]) | ||
679 | count_would_be_workdays = len([1 for day in days if day.year == year and worktime.would_be_workday(day) and day not in worktime.leave_days]) | ||
680 | row_data.append(year_offset[year]) | ||
681 | if len(year_classification[year]) != count_would_be_workdays: | ||
682 | row_data.append(f"{count_classified}/{len(year_classification[year])}/{count_would_be_workdays}") | ||
683 | else: | ||
684 | row_data.append(f"{count_classified}/{len(year_classification[year])}") | ||
685 | row_data.append(','.join(sorted([day.strftime('%m-%d') for day, classified in year_classification[year].items() if classified]))) | ||
686 | table_data.append(row_data) | ||
687 | print(tabulate(table_data, tablefmt="plain")) | ||
688 | |||
597 | def main(): | 689 | def main(): |
690 | config = Worktime.config() | ||
691 | |||
598 | parser = argparse.ArgumentParser(prog = "worktime", description = 'Track worktime using toggl API') | 692 | parser = argparse.ArgumentParser(prog = "worktime", description = 'Track worktime using toggl API') |
599 | 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())) | 693 | 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())) |
600 | parser.add_argument('--no-running', dest = 'include_running', action = 'store_false') | 694 | parser.add_argument('--no-running', dest = 'include_running', action = 'store_false') |
@@ -613,6 +707,10 @@ def main(): | |||
613 | leave_parser.add_argument('year', metavar = 'YEAR', type = int, help = 'Year to evaluate leave days for (default: current year)', default = None, nargs='?') | 707 | leave_parser.add_argument('year', metavar = 'YEAR', type = int, help = 'Year to evaluate leave days for (default: current year)', default = None, nargs='?') |
614 | leave_parser.add_argument('--table', action = 'store_true') | 708 | leave_parser.add_argument('--table', action = 'store_true') |
615 | leave_parser.set_defaults(cmd = leave) | 709 | leave_parser.set_defaults(cmd = leave) |
710 | for classification_name in config.get('day-classification', {}).keys(): | ||
711 | classification_parser = subparsers.add_parser(classification_name) | ||
712 | classification_parser.add_argument('--table', action = 'store_true') | ||
713 | classification_parser.set_defaults(cmd = partial(classification, classification_name=classification_name)) | ||
616 | args = parser.parse_args() | 714 | args = parser.parse_args() |
617 | 715 | ||
618 | args.cmd(**vars(args)) | 716 | args.cmd(**vars(args)) |