|
| 1 | +import json |
| 2 | +import re |
| 3 | +from datetime import datetime, timedelta, timezone, time |
| 4 | +from typing import List, Tuple, Dict |
| 5 | + |
| 6 | +from tabulate import tabulate |
| 7 | + |
| 8 | +import aw_client |
| 9 | +from aw_core import Event |
| 10 | +from aw_transform import flood |
| 11 | + |
| 12 | + |
| 13 | +def _pretty_timedelta(td: timedelta) -> str: |
| 14 | + s = str(td) |
| 15 | + s = re.sub(r"^(0+[:]?)+", "", s) |
| 16 | + s = s.rjust(len(str(td)), " ") |
| 17 | + s = re.sub(r"[.]\d+", "", s) |
| 18 | + return s |
| 19 | + |
| 20 | + |
| 21 | +assert _pretty_timedelta(timedelta(seconds=120)) == " 2:00" |
| 22 | +assert _pretty_timedelta(timedelta(hours=9, minutes=5)) == "9:05:00" |
| 23 | + |
| 24 | + |
| 25 | +def generous_approx(events: List[dict], max_break: float) -> timedelta: |
| 26 | + """ |
| 27 | + Returns a generous approximation of worked time by including non-categorized time when shorter than a specific duration |
| 28 | +
|
| 29 | + max_break: Max time (in seconds) to flood when there's an empty slot between events |
| 30 | + """ |
| 31 | + events_e: List[Event] = [Event(**e) for e in events] |
| 32 | + return sum( |
| 33 | + map(lambda e: e.duration, flood(events_e, max_break)), |
| 34 | + timedelta(), |
| 35 | + ) |
| 36 | + |
| 37 | + |
| 38 | +def query(): |
| 39 | + td1d = timedelta(days=1) |
| 40 | + day_offset = timedelta(hours=4) |
| 41 | + |
| 42 | + now = datetime.now(tz=timezone.utc) |
| 43 | + # TODO: Account for timezone, or maybe it's handled correctly by aw_client? |
| 44 | + today = datetime.combine(now.date(), time()) + day_offset |
| 45 | + |
| 46 | + timeperiods = [(today - i * td1d, today - (i - 1) * td1d) for i in range(5)] |
| 47 | + timeperiods.reverse() |
| 48 | + |
| 49 | + categories: List[Tuple[List[str], Dict]] = [ |
| 50 | + ( |
| 51 | + ["Work"], |
| 52 | + { |
| 53 | + "type": "regex", |
| 54 | + "regex": r"activitywatch|algobit|defiarb|github.com", |
| 55 | + "ignore_case": True, |
| 56 | + }, |
| 57 | + ) |
| 58 | + ] |
| 59 | + |
| 60 | + aw = aw_client.ActivityWatchClient() |
| 61 | + |
| 62 | + # TODO: Move this query somewhere else, as the equivalent of aw-webui's 'canonicalEvents' |
| 63 | + res = aw.query( |
| 64 | + f""" |
| 65 | + window = flood(query_bucket(find_bucket("aw-watcher-window_"))); |
| 66 | + afk = flood(query_bucket(find_bucket("aw-watcher-afk_"))); |
| 67 | + events = filter_period_intersect(window, filter_keyvals(afk, "status", ["not-afk"])); |
| 68 | + events = categorize(events, {json.dumps(categories)}); |
| 69 | + events = filter_keyvals(events, "$category", [["Work"]]); |
| 70 | + duration = sum_durations(events); |
| 71 | + RETURN = {{"events": events, "duration": duration}}; |
| 72 | + """, |
| 73 | + timeperiods, |
| 74 | + ) |
| 75 | + |
| 76 | + for break_time in [0, 5 * 60, 10 * 60, 15 * 60]: |
| 77 | + _print( |
| 78 | + timeperiods, res, break_time, {"category_rule": categories[0][1]["regex"]} |
| 79 | + ) |
| 80 | + |
| 81 | + save = True |
| 82 | + if save: |
| 83 | + fn = "working_hours_events.json" |
| 84 | + with open(fn, "w") as f: |
| 85 | + print(f"Saving to {fn}...") |
| 86 | + json.dump(res, f, indent=2) |
| 87 | + |
| 88 | + |
| 89 | +def _print(timeperiods, res, break_time, params: dict): |
| 90 | + print("Using:") |
| 91 | + print(f" break_time={break_time}") |
| 92 | + print("\n".join(f" {key}={val}" for key, val in params.items())) |
| 93 | + print( |
| 94 | + tabulate( |
| 95 | + [ |
| 96 | + [ |
| 97 | + start.date(), |
| 98 | + # Without flooding: |
| 99 | + # _pretty_timedelta(timedelta(seconds=res[i]["duration"])), |
| 100 | + # With flooding: |
| 101 | + _pretty_timedelta(generous_approx(res[i]["events"], break_time)), |
| 102 | + len(res[i]["events"]), |
| 103 | + ] |
| 104 | + for i, (start, stop) in enumerate(timeperiods) |
| 105 | + ], |
| 106 | + headers=["Date", "Duration", "Events"], |
| 107 | + colalign=( |
| 108 | + "left", |
| 109 | + "right", |
| 110 | + ), |
| 111 | + ) |
| 112 | + ) |
| 113 | + |
| 114 | + print( |
| 115 | + f"Total: {sum((generous_approx(res[i]['events'], break_time) for i in range(len(timeperiods))), timedelta())}" |
| 116 | + ) |
| 117 | + print("") |
| 118 | + |
| 119 | + |
| 120 | +if __name__ == "__main__": |
| 121 | + query() |
0 commit comments