Skip to content

Commit 4efe542

Browse files
committed
feat: use server-side categories, invoke start as default subcommand
1 parent c2168b6 commit 4efe542

File tree

1 file changed

+12
-101
lines changed

1 file changed

+12
-101
lines changed

aw_notify/main.py

Lines changed: 12 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
and send notifications to the user on predefined conditions.
44
"""
55
import logging
6-
import os
76
import threading
87
from collections import defaultdict
98
from datetime import datetime, timedelta, timezone
@@ -19,99 +18,15 @@
1918

2019
import aw_client.queries
2120
import click
22-
import tomlkit
2321
from desktop_notifier import DesktopNotifier
2422
from typing_extensions import TypeAlias
2523

2624
# TODO: Add thresholds for total time today (incl percentage of productive time)
2725

2826
logger = logging.getLogger(__name__)
29-
TIME_OFFSET = timedelta(hours=4)
30-
FALLBACK_CATEGORIES: list[tuple[list[str], dict]] = [
31-
(
32-
["Work"],
33-
{
34-
"type": "regex",
35-
"regex": "Programming|nvim|taxes|Roam|Code",
36-
},
37-
),
38-
(
39-
["Twitter"],
40-
{
41-
"type": "regex",
42-
"regex": r"Twitter|twitter.com|Home / X",
43-
},
44-
),
45-
(
46-
["Youtube"],
47-
{
48-
"type": "regex",
49-
"regex": r"Youtube|youtube.com",
50-
},
51-
),
52-
]
53-
54-
55-
# TODO: move to aw-client utils
56-
# TODO: Get categories from aw-webui export (in the future from server key-val store)
57-
def load_category_toml(path: Path) -> list[tuple[list[str], dict]]:
58-
with open(path, "r") as f:
59-
toml = tomlkit.load(f)
60-
return parse_category_toml(toml, parent=[])
61-
62-
63-
def parse_category_toml(toml: dict, parent: list[str]) -> list[tuple[list[str], dict]]:
64-
"""
65-
Parse category config file and return a list of categories.
66-
"""
67-
categories = []
68-
if "categories" in toml:
69-
toml = toml["categories"]
70-
for cat_name, cat in toml.items():
71-
if isinstance(cat, dict):
72-
categories += parse_category_toml(cat, parent=parent + [cat_name])
73-
else:
74-
if cat_name == "$re":
75-
categories.append((parent, {"type": "regex", "regex": cat}))
76-
else:
77-
categories.append(
78-
(parent + [cat_name], {"type": "regex", "regex": cat})
79-
)
80-
# create parent category with no rule if $re not given
81-
if parent and parent not in (c for c, _ in categories):
82-
categories.append((parent, {"type": "none"}))
83-
return sorted(categories)
84-
85-
86-
def test_parse_category_toml():
87-
"""
88-
Test parsing of category config file.
89-
"""
90-
# Example category config file:
91-
config = """
92-
[categories]
93-
[categories.Media]
94-
Music = '[Ss]potify|[Ss]ound[Cc]loud|Mixxx|Shazam'
95-
[categories.Media.Games]
96-
'$re' = 'Video Games'
97-
Steam = 'Steam'
98-
"""
99-
categories = parse_category_toml(tomlkit.loads(config), parent=[])
100-
101-
# Check that "Media" category exists
102-
assert categories[0][0] == ["Media"]
103-
104-
# Check that "Games" category exists, and has the correct regex
105-
assert categories[1][0] == ["Media", "Games"]
106-
assert categories[1][1]["regex"] == "Video Games"
107-
assert categories[2][0] == ["Media", "Games", "Steam"]
108-
assert categories[2][1]["regex"] == "Steam"
109-
11027

111-
CATEGORIES = FALLBACK_CATEGORIES
112-
113-
114-
time_offset = timedelta(hours=4)
28+
# TODO: read from server settings
29+
TIME_OFFSET = timedelta(hours=4)
11530

11631
aw = aw_client.ActivityWatchClient("aw-notify", testing=False)
11732

@@ -155,8 +70,8 @@ def get_time(date=None) -> dict[str, timedelta]:
15570
date = date.replace(hour=0, minute=0, second=0, microsecond=0)
15671
timeperiods = [
15772
(
158-
date + time_offset,
159-
date + time_offset + timedelta(days=1),
73+
date + TIME_OFFSET,
74+
date + TIME_OFFSET + timedelta(days=1),
16075
)
16176
]
16277

@@ -165,7 +80,6 @@ def get_time(date=None) -> dict[str, timedelta]:
16580
aw_client.queries.DesktopQueryParams(
16681
bid_window=f"aw-watcher-window_{hostname}",
16782
bid_afk=f"aw-watcher-afk_{hostname}",
168-
classes=CATEGORIES,
16983
)
17084
)
17185
query = f"""
@@ -277,7 +191,7 @@ def time_to_next_threshold(self) -> timedelta:
277191
)
278192
if day_end < datetime.now(timezone.utc):
279193
day_end += timedelta(days=1)
280-
time_to_next_day = day_end - datetime.now(timezone.utc) + time_offset
194+
time_to_next_day = day_end - datetime.now(timezone.utc) + TIME_OFFSET
281195
return time_to_next_day + min(self.thresholds)
282196

283197
return min(self.thresholds_untriggered) - self.time_spent
@@ -328,9 +242,10 @@ def test_category_alert():
328242
catalert.check()
329243

330244

331-
@click.group()
245+
@click.group(invoke_without_command=True)
246+
@click.pass_context
332247
@click.option("-v", "--verbose", is_flag=True, help="Enables verbose mode.")
333-
def main(verbose: bool):
248+
def main(ctx, verbose: bool):
334249
logging.basicConfig(
335250
level=logging.DEBUG if verbose else logging.INFO,
336251
format="%(asctime)s [%(levelname)5s] %(message)s"
@@ -339,11 +254,8 @@ def main(verbose: bool):
339254
)
340255
logging.getLogger("urllib3").setLevel(logging.WARNING)
341256

342-
AW_CATEGORY_PATH = os.environ.get("AW_CATEGORY_PATH", None)
343-
if AW_CATEGORY_PATH:
344-
global CATEGORIES
345-
CATEGORIES = load_category_toml(Path(AW_CATEGORY_PATH))
346-
logger.info("Loaded categories from $AW_CATEGORY_PATH")
257+
if ctx.invoked_subcommand is None:
258+
ctx.invoke(start)
347259

348260

349261
@main.command()
@@ -398,9 +310,8 @@ def send_checkin(title="Time today", date=None):
398310
Sends a summary notification of the day.
399311
Meant to be sent at a particular time, like at the end of a working day (e.g. 5pm).
400312
"""
401-
categories = list(
402-
set(["All", "Uncategorized"] + [">".join(k) for k, _ in CATEGORIES])
403-
)
313+
# TODO: make configurable which categories to show
314+
categories = list(set(["All", "Work", "Uncategorized"]))
404315
cat_time = get_time(date=date)
405316
time_spent = [cat_time.get(c, timedelta()) for c in categories]
406317
top_categories = [

0 commit comments

Comments
 (0)