snapshot of https://github.com/ActivityWatch/aw-client.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build / Test on ubuntu-latest (push) Successful in 2m4s
Examples / Test examples (ubuntu-latest, 3.10) (push) Successful in 2m16s
Lint / lint (push) Successful in 1m25s
Build / Test on macOS-latest (push) Has been cancelled
Build / Test on windows-latest (push) Has been cancelled
CodeQL / Analyze (python) (push) Failing after 1s
Some checks failed
Build / Test on ubuntu-latest (push) Successful in 2m4s
Examples / Test examples (ubuntu-latest, 3.10) (push) Successful in 2m16s
Lint / lint (push) Successful in 1m25s
Build / Test on macOS-latest (push) Has been cancelled
Build / Test on windows-latest (push) Has been cancelled
CodeQL / Analyze (python) (push) Failing after 1s
This commit is contained in:
157
examples/working_hours.py
Normal file
157
examples/working_hours.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Script that computes how many hours was spent in a regex-specified "work" category for each day in a given month.
|
||||
|
||||
Also saves the matching work-events to a JSON file (for auditing purposes).
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
from datetime import datetime, time, timedelta
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import aw_client
|
||||
from aw_client import queries
|
||||
from aw_core import Event
|
||||
from aw_transform import flood
|
||||
from tabulate import tabulate
|
||||
|
||||
OUTPUT_HTML = os.environ.get("OUTPUT_HTML", "").lower() == "true"
|
||||
|
||||
td1d = timedelta(days=1)
|
||||
day_offset = timedelta(hours=4)
|
||||
|
||||
|
||||
def _pretty_timedelta(td: timedelta) -> str:
|
||||
s = str(td)
|
||||
s = re.sub(r"^(0+[:]?)+", "", s)
|
||||
s = s.rjust(len(str(td)), " ")
|
||||
s = re.sub(r"[.]\d+", "", s)
|
||||
return s
|
||||
|
||||
|
||||
assert _pretty_timedelta(timedelta(seconds=120)) == " 2:00"
|
||||
assert _pretty_timedelta(timedelta(hours=9, minutes=5)) == "9:05:00"
|
||||
|
||||
|
||||
def generous_approx(events: List[dict], max_break: float) -> timedelta:
|
||||
"""
|
||||
Returns a generous approximation of worked time by including non-categorized time when shorter than a specific duration
|
||||
|
||||
max_break: Max time (in seconds) to flood when there's an empty slot between events
|
||||
"""
|
||||
events_e: List[Event] = [Event(**e) for e in events]
|
||||
return sum(
|
||||
map(lambda e: e.duration, flood(events_e, max_break)),
|
||||
timedelta(),
|
||||
)
|
||||
|
||||
|
||||
def query(regex: str, timeperiods, hostname: str):
|
||||
print("Querying events...")
|
||||
print(f" Day offset: {day_offset}")
|
||||
print("")
|
||||
|
||||
categories: List[Tuple[List[str], Dict]] = [
|
||||
(
|
||||
["Work"],
|
||||
{
|
||||
"type": "regex",
|
||||
"regex": regex,
|
||||
"ignore_case": True,
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
aw = aw_client.ActivityWatchClient(client_name="working_hours")
|
||||
|
||||
canonicalQuery = queries.canonicalEvents(
|
||||
queries.DesktopQueryParams(
|
||||
bid_window=f"aw-watcher-window_{hostname}",
|
||||
bid_afk=f"aw-watcher-afk_{hostname}",
|
||||
classes=categories,
|
||||
filter_classes=[["Work"]],
|
||||
)
|
||||
)
|
||||
query = f"""
|
||||
{canonicalQuery}
|
||||
duration = sum_durations(events);
|
||||
RETURN = {{"events": events, "duration": duration}};
|
||||
"""
|
||||
|
||||
res = aw.query(query, timeperiods)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 working_hours.py <regex> [hostname]")
|
||||
exit(1)
|
||||
|
||||
regex = sys.argv[1]
|
||||
print(f"Using regex: {regex}")
|
||||
|
||||
if len(sys.argv) > 2:
|
||||
hostname = sys.argv[2]
|
||||
print(f"Using hostname: {hostname}")
|
||||
else:
|
||||
hostname = socket.gethostname()
|
||||
|
||||
now = datetime.now().astimezone()
|
||||
today = (datetime.combine(now.date(), time()) + day_offset).astimezone()
|
||||
|
||||
timeperiods = [(today - i * td1d, today - (i - 1) * td1d) for i in range(5)]
|
||||
timeperiods.reverse()
|
||||
|
||||
res = query(regex, timeperiods, hostname)
|
||||
|
||||
for break_time in [0, 5 * 60, 15 * 60]:
|
||||
_print(timeperiods, res, break_time, {"regex": regex})
|
||||
|
||||
fn = "working_hours_events.json"
|
||||
with open(fn, "w") as f:
|
||||
print(f"Saving to {fn}...")
|
||||
json.dump(res, f, indent=2)
|
||||
|
||||
|
||||
def _print(timeperiods, res, break_time, params: dict):
|
||||
print("Using:")
|
||||
print(f" break_time={break_time}")
|
||||
print("\n".join(f" {key}={val}" for key, val in params.items()))
|
||||
print(
|
||||
tabulate(
|
||||
[
|
||||
[
|
||||
start.date(),
|
||||
# Without flooding:
|
||||
# _pretty_timedelta(timedelta(seconds=res[i]["duration"])),
|
||||
# With flooding:
|
||||
_pretty_timedelta(generous_approx(res[i]["events"], break_time)),
|
||||
len(res[i]["events"]),
|
||||
]
|
||||
for i, (start, stop) in enumerate(timeperiods)
|
||||
],
|
||||
headers=["Date", "Duration", "Events"],
|
||||
colalign=(
|
||||
"left",
|
||||
"right",
|
||||
),
|
||||
tablefmt="html" if OUTPUT_HTML else "simple",
|
||||
)
|
||||
)
|
||||
|
||||
print(
|
||||
f"Total: {sum((generous_approx(res[i]['events'], break_time) for i in range(len(timeperiods))), timedelta())}"
|
||||
)
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# ignore log warnings in aw_transform
|
||||
logging.getLogger("aw_transform").setLevel(logging.ERROR)
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user