From 6fbce36332b023d9801cbab62719871f4607ed71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 20 Jan 2021 13:35:14 +0100 Subject: [PATCH] feat: added example script for calculating working hours --- .gitignore | 1 + examples/working_hours.py | 121 ++++++++++++++++++++++++++++++++++++++ poetry.lock | 17 +++++- pyproject.toml | 1 + 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 examples/working_hours.py diff --git a/.gitignore b/.gitignore index d5cce90..82e2a06 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build dist *.swp .*cache +*.json diff --git a/examples/working_hours.py b/examples/working_hours.py new file mode 100644 index 0000000..2e9aeda --- /dev/null +++ b/examples/working_hours.py @@ -0,0 +1,121 @@ +import json +import re +from datetime import datetime, timedelta, timezone, time +from typing import List, Tuple, Dict + +from tabulate import tabulate + +import aw_client +from aw_core import Event +from aw_transform import flood + + +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(): + td1d = timedelta(days=1) + day_offset = timedelta(hours=4) + + now = datetime.now(tz=timezone.utc) + # TODO: Account for timezone, or maybe it's handled correctly by aw_client? + today = datetime.combine(now.date(), time()) + day_offset + + timeperiods = [(today - i * td1d, today - (i - 1) * td1d) for i in range(5)] + timeperiods.reverse() + + categories: List[Tuple[List[str], Dict]] = [ + ( + ["Work"], + { + "type": "regex", + "regex": r"activitywatch|algobit|defiarb|github.com", + "ignore_case": True, + }, + ) + ] + + aw = aw_client.ActivityWatchClient() + + # TODO: Move this query somewhere else, as the equivalent of aw-webui's 'canonicalEvents' + res = aw.query( + f""" + window = flood(query_bucket(find_bucket("aw-watcher-window_"))); + afk = flood(query_bucket(find_bucket("aw-watcher-afk_"))); + events = filter_period_intersect(window, filter_keyvals(afk, "status", ["not-afk"])); + events = categorize(events, {json.dumps(categories)}); + events = filter_keyvals(events, "$category", [["Work"]]); + duration = sum_durations(events); + RETURN = {{"events": events, "duration": duration}}; + """, + timeperiods, + ) + + for break_time in [0, 5 * 60, 10 * 60, 15 * 60]: + _print( + timeperiods, res, break_time, {"category_rule": categories[0][1]["regex"]} + ) + + save = True + if save: + 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", + ), + ) + ) + + print( + f"Total: {sum((generous_approx(res[i]['events'], break_time) for i in range(len(timeperiods))), timedelta())}" + ) + print("") + + +if __name__ == "__main__": + query() diff --git a/poetry.lock b/poetry.lock index 6892bbf..78c178a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -385,6 +385,17 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "tabulate" +version = "0.8.7" +description = "Pretty-print tabular data" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +widechars = ["wcwidth"] + [[package]] name = "takethetime" version = "0.3.1" @@ -467,7 +478,7 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pyt [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "1931ee63d8500a6a307a4b9e1007fb7125b937b306cf45af145340f8333e7049" +content-hash = "f4bb32f9a668e8bcbaf82027d174bfcf43753fa62f8f43f13ad42ae60572af77" [metadata.files] appdirs = [ @@ -678,6 +689,10 @@ six = [ strict-rfc3339 = [ {file = "strict-rfc3339-0.7.tar.gz", hash = "sha256:5cad17bedfc3af57b399db0fed32771f18fc54bbd917e85546088607ac5e1277"}, ] +tabulate = [ + {file = "tabulate-0.8.7-py3-none-any.whl", hash = "sha256:ac64cb76d53b1231d364babcd72abbb16855adac7de6665122f97b593f1eb2ba"}, + {file = "tabulate-0.8.7.tar.gz", hash = "sha256:db2723a20d04bcda8522165c73eea7c300eda74e0ce852d9022e0159d7895007"}, +] takethetime = [ {file = "TakeTheTime-0.3.1.tar.gz", hash = "sha256:dbe30453a1b596a38f9e2e3fa8e1adc5af2dbf646ca0837ad5c2059a16fe2ff9"}, ] diff --git a/pyproject.toml b/pyproject.toml index d8c9a9b..e67aaff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ pytest = "^6.0" pytest-cov = "^2.8.1" mypy = "*" pylint = "^2.4.4" +tabulate = "^0.8.7" [build-system] requires = ["poetry>=0.12"]