Skip to content

Commit 6fbce36

Browse files
committed
feat: added example script for calculating working hours
1 parent 86c9d7f commit 6fbce36

4 files changed

Lines changed: 139 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ build
55
dist
66
*.swp
77
.*cache
8+
*.json

examples/working_hours.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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()

poetry.lock

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pytest = "^6.0"
2727
pytest-cov = "^2.8.1"
2828
mypy = "*"
2929
pylint = "^2.4.4"
30+
tabulate = "^0.8.7"
3031

3132
[build-system]
3233
requires = ["poetry>=0.12"]

0 commit comments

Comments
 (0)