Skip to content

Commit 2cb09e7

Browse files
committed
feat(example): added email_report example (+ improvements to working_hours example)
1 parent b9024b1 commit 2cb09e7

File tree

3 files changed

+113
-4
lines changed

3 files changed

+113
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ dist
66
*.swp
77
.*cache
88
*.json
9+
secret.env

examples/email_report.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""
2+
Sends an email with a summary report (WIP)
3+
4+
For now, it just sends stdin.
5+
In the future, it will generate a pretty email and send that.
6+
7+
Requires that an SMTP server is configured through environment variables.
8+
9+
Example usage:
10+
11+
$ env OUTPUT_HTML=true python3 examples/working_hours.py | python3 examples/email_report.py
12+
"""
13+
14+
import smtplib
15+
import os
16+
import sys
17+
from dataclasses import dataclass
18+
from email.mime.text import MIMEText
19+
from email.mime.multipart import MIMEMultipart
20+
21+
22+
@dataclass
23+
class Recipient:
24+
name: str
25+
email: str
26+
27+
28+
def create_msg(
29+
sender: Recipient,
30+
receiver: Recipient,
31+
subject: str,
32+
text: str,
33+
html=None,
34+
) -> MIMEMultipart:
35+
"""Based on https://stackoverflow.com/a/882770/965332"""
36+
msg = MIMEMultipart("alternative")
37+
msg["Subject"] = subject
38+
msg["From"] = sender.email
39+
msg["To"] = receiver.email
40+
41+
# Record the MIME types of both parts - text/plain and text/html.
42+
# Also attach parts into message container.
43+
# According to RFC 2046, the last part of a multipart message, in this case
44+
# the HTML message, is best and preferred.
45+
msg.attach(MIMEText(text, "plain"))
46+
if html:
47+
msg.attach(MIMEText(html, "html"))
48+
49+
return msg
50+
51+
52+
def main(read_stdin=True) -> None:
53+
smtp_server = os.environ["SMTP_SERVER"].strip()
54+
smtp_username = os.environ["SMTP_USERNAME"].strip()
55+
smtp_password = os.environ["SMTP_PASSWORD"].strip()
56+
57+
assert smtp_server, "Enviroment variable SMTP_SERVER not set"
58+
assert smtp_username, "Enviroment variable SMTP_USERNAME not set"
59+
assert smtp_password, "Enviroment variable SMTP_PASSWORD not set"
60+
61+
sender = Recipient("ActivityWatch (automated script)", "noreply@activitywatch.net")
62+
receiver = Recipient("Erik Bjäreholt", "erik.bjareholt@gmail.com")
63+
64+
if read_stdin:
65+
# Accepts input from stdin
66+
text = sys.stdin.read()
67+
else:
68+
text = "Just a test. ActivityWatch stats will go here."
69+
70+
text = text.replace("\n\n", "<hr>")
71+
# text = text.replace("\n\n", "<br>")
72+
# Create the body of the message (a plain-text and an HTML version).
73+
html = f"""\
74+
<html>
75+
<head></head>
76+
<body>
77+
<p>{text}</p>
78+
</body>
79+
</html>
80+
"""
81+
82+
subject = "Example report from aw-client"
83+
msg = create_msg(sender, receiver, subject, text, html)
84+
85+
try:
86+
with smtplib.SMTP_SSL(smtp_server, 465) as smtp:
87+
smtp.login(smtp_username, smtp_password)
88+
smtp.send_message(msg)
89+
smtp.quit()
90+
print("Successfully sent email")
91+
except smtplib.SMTPException as e:
92+
print("Error: unable to send email")
93+
print(e)
94+
95+
96+
if __name__ == "__main__":
97+
main()

examples/working_hours.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
"""
2+
Script that computes how many hours was spent in a regex-specified "work" category for each day in a given month.
3+
4+
Also saves the matching work-events to a JSON file (for auditing purposes).
5+
"""
6+
17
import json
28
import re
3-
from datetime import datetime, timedelta, timezone, time
9+
import os
10+
from datetime import datetime, timedelta, time
411
from typing import List, Tuple, Dict
512

613
from tabulate import tabulate
@@ -10,6 +17,10 @@
1017
from aw_transform import flood
1118

1219

20+
EXAMPLE_REGEX = r"activitywatch|algobit|defiarb|github.com"
21+
OUTPUT_HTML = os.environ.get("OUTPUT_HTML", "").lower() == "true"
22+
23+
1324
def _pretty_timedelta(td: timedelta) -> str:
1425
s = str(td)
1526
s = re.sub(r"^(0+[:]?)+", "", s)
@@ -35,7 +46,7 @@ def generous_approx(events: List[dict], max_break: float) -> timedelta:
3546
)
3647

3748

38-
def query():
49+
def query(regex: str = EXAMPLE_REGEX, save=True):
3950
print("Querying events...")
4051
td1d = timedelta(days=1)
4152
day_offset = timedelta(hours=4)
@@ -53,7 +64,7 @@ def query():
5364
["Work"],
5465
{
5566
"type": "regex",
56-
"regex": r"activitywatch|algobit|defiarb|github.com",
67+
"regex": regex,
5768
"ignore_case": True,
5869
},
5970
)
@@ -80,7 +91,6 @@ def query():
8091
timeperiods, res, break_time, {"category_rule": categories[0][1]["regex"]}
8192
)
8293

83-
save = True
8494
if save:
8595
fn = "working_hours_events.json"
8696
with open(fn, "w") as f:
@@ -110,6 +120,7 @@ def _print(timeperiods, res, break_time, params: dict):
110120
"left",
111121
"right",
112122
),
123+
tablefmt="html" if OUTPUT_HTML else "simple",
113124
)
114125
)
115126

0 commit comments

Comments
 (0)