Skip to content

Commit 020cab2

Browse files
authored
Merge pull request #4 from ilias-ant/feature/ia/email-callback
EmailCallback
2 parents 0c90e18 + 5ef2853 commit 020cab2

File tree

11 files changed

+266
-23
lines changed

11 files changed

+266
-23
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ The following apps are currently supported. But, do check the project frequently
2929
<td>
3030
<img src="https://raw.githubusercontent.com/ilias-ant/tf-notify/main/static/logos/telegram.png" height="128" width="128" style="max-height: 128px; max-width: 128px;"><a href="https://tf-notify.readthedocs.io/en/latest/api/#tf_notify.callbacks.telegram.TelegramCallback">Telegram</a>
3131
</td>
32+
<td>
33+
<img src="https://raw.githubusercontent.com/ilias-ant/tf-notify/main/static/logos/email.png" height="128" width="128" style="max-height: 128px; max-width: 128px;"><a href="https://tf-notify.readthedocs.io/en/latest/api/#tf_notify.callbacks.email.EmailCallback">Email (SMTP)</a>
34+
</td>
3235
</tr>
3336
</table>
3437

docs/api.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@
66

77
::: tf_notify.callbacks.telegram.TelegramCallback
88
rendering:
9-
show_root_heading: true
9+
show_root_heading: true
10+
11+
::: tf_notify.callbacks.email.EmailCallback
12+
rendering:
13+
show_root_heading: true

docs/index.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ The following apps are currently supported. But, do check the project frequently
2121
<td>
2222
<img src="https://raw.githubusercontent.com/ilias-ant/tf-notify/main/static/logos/telegram.png" height="128" width="128" style="max-height: 128px; max-width: 128px;"><a href="https://tf-notify.readthedocs.io/en/latest/api/#tf_notify.callbacks.telegram.TelegramCallback">Telegram</a>
2323
</td>
24+
<td>
25+
<img src="https://raw.githubusercontent.com/ilias-ant/tf-notify/main/static/logos/email.png" height="128" width="128" style="max-height: 128px; max-width: 128px;"><a href="https://tf-notify.readthedocs.io/en/latest/api/#tf_notify.callbacks.email.EmailCallback">Email (SMTP)</a>
26+
</td>
2427
</tr>
2528
</table>
26-

poetry.lock

Lines changed: 17 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "tf-notify"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
description = "Want to get notified on the progress of your TensorFlow model training? Enter, a TensorFlow Keras callback to send notifications on the messaging app of your choice."
55
authors = ["Ilias Antonopoulos <ilias.antonopoulos@yahoo.gr>"]
66
readme = "README.md"

static/logos/email.png

72.4 KB
Loading

tests/callbacks/test_email.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import sys
2+
from unittest import mock
3+
4+
import pytest
5+
import tensorflow as tf
6+
7+
from tf_notify import EmailCallback
8+
9+
10+
class TestEmailCallback:
11+
@pytest.mark.skipif(
12+
sys.version_info < (3, 8), reason="requires python3.8 or higher"
13+
)
14+
def test_callback_occurs_on_train_end(self):
15+
16+
# define tf.keras model to add callback to
17+
model = tf.keras.Sequential(name="neural-network")
18+
model.add(tf.keras.layers.Dense(1, input_dim=784))
19+
model.compile(
20+
optimizer=tf.keras.optimizers.RMSprop(learning_rate=0.1),
21+
loss="mean_squared_error",
22+
metrics=["mean_absolute_error"],
23+
)
24+
25+
# load example MNIST data and pre-process it
26+
(x_train, y_train), _ = tf.keras.datasets.mnist.load_data()
27+
x_train = x_train.reshape(-1, 784).astype("float32") / 255.0
28+
29+
# limit the data to 1000 samples
30+
x_train = x_train[:1000]
31+
y_train = y_train[:1000]
32+
33+
with mock.patch("smtplib.SMTP", autospec=True) as smtp_mock:
34+
# initiate training
35+
model.fit(
36+
x_train,
37+
y_train,
38+
batch_size=128,
39+
epochs=2,
40+
verbose=0,
41+
validation_split=0.5,
42+
callbacks=[
43+
EmailCallback(
44+
to="toaddress@yahoo.com",
45+
from_="fromaddress@yahoo.com",
46+
host="smtp.mail.yahoo.com",
47+
port=465,
48+
username="my-cool-username",
49+
password="my-cool-password", # one-time app password
50+
ssl=False,
51+
)
52+
],
53+
)
54+
55+
# validate smtplib.SMTP was called twice
56+
assert len(smtp_mock.method_calls) == 2
57+
58+
login, send_message = smtp_mock.method_calls
59+
60+
# one time to authenticate against the SMTP server
61+
assert login.args == (
62+
"my-cool-username",
63+
"my-cool-password",
64+
) # .args available in py3.8+, this fails on py3.7
65+
66+
# and another, to send the message
67+
email = send_message.args[0]
68+
components = dict(email.items())
69+
70+
assert components["To"] == "toaddress@yahoo.com"
71+
assert components["From"] == "fromaddress@yahoo.com"
72+
assert components["Subject"] == "New mail from 'tf-notify'!"
73+
74+
payload = email.get_payload()[0].as_string()
75+
76+
assert "model <neural-network> has completed its training!" in payload
77+
78+
@pytest.mark.skipif(
79+
sys.version_info < (3, 8), reason="requires python3.8 or higher"
80+
)
81+
def test_callback_occurs_on_train_end_while_both_message_and_subject_are_overridden(
82+
self,
83+
):
84+
85+
# define tf.keras model to add callback to
86+
model = tf.keras.Sequential(name="neural-network")
87+
model.add(tf.keras.layers.Dense(1, input_dim=784))
88+
model.compile(
89+
optimizer=tf.keras.optimizers.RMSprop(learning_rate=0.1),
90+
loss="mean_squared_error",
91+
metrics=["mean_absolute_error"],
92+
)
93+
94+
# load example MNIST data and pre-process it
95+
(x_train, y_train), _ = tf.keras.datasets.mnist.load_data()
96+
x_train = x_train.reshape(-1, 784).astype("float32") / 255.0
97+
98+
# limit the data to 1000 samples
99+
x_train = x_train[:1000]
100+
y_train = y_train[:1000]
101+
102+
with mock.patch("smtplib.SMTP", autospec=True) as smtp_mock:
103+
# initiate training
104+
model.fit(
105+
x_train,
106+
y_train,
107+
batch_size=128,
108+
epochs=2,
109+
verbose=0,
110+
validation_split=0.5,
111+
callbacks=[
112+
EmailCallback(
113+
message="This is a custom message!",
114+
subject="You have mail!",
115+
to="toaddress@yahoo.com",
116+
from_="fromaddress@yahoo.com",
117+
host="smtp.mail.yahoo.com",
118+
port=465,
119+
username="my-cool-username",
120+
password="my-cool-password", # one-time app password
121+
ssl=False, # if this is True, patching of smtplib.SMTP_SSL is needed instead
122+
)
123+
],
124+
)
125+
126+
# validate smtplib.SMTP was called twice
127+
assert len(smtp_mock.method_calls) == 2
128+
129+
login, send_message = smtp_mock.method_calls
130+
131+
# one time to authenticate against the SMTP server
132+
assert login.args == (
133+
"my-cool-username",
134+
"my-cool-password",
135+
) # .args available in py3.8+, this fails on py3.7
136+
137+
# and another, to send the message
138+
email = send_message.args[0]
139+
components = dict(email.items())
140+
141+
assert components["To"] == "toaddress@yahoo.com"
142+
assert components["From"] == "fromaddress@yahoo.com"
143+
assert components["Subject"] == "You have mail!"
144+
145+
payload = email.get_payload()[0].as_string()
146+
147+
assert "This is a custom message!" in payload

tf_notify/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
from .callbacks.email import EmailCallback
12
from .callbacks.slack import SlackCallback
23
from .callbacks.telegram import TelegramCallback
34

4-
__version__ = "0.2.0"
5+
__version__ = "0.3.0"
56

6-
__all__ = ["__version__", "SlackCallback", "TelegramCallback"]
7+
__all__ = ["__version__", "EmailCallback", "SlackCallback", "TelegramCallback"]

tf_notify/callbacks/email.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from typing import Any
2+
3+
from notifiers import get_notifier
4+
5+
from .base import BaseNotificationCallback
6+
7+
8+
class EmailCallback(BaseNotificationCallback):
9+
"""
10+
A custom tf.callbacks.Callback that enables sending email messages to SMTP servers.
11+
12+
Keep in mind that in order to authenticate against an SMTP server (e.g. *smtp.mail.yahoo.com*),
13+
you will have to first generate a one-time password (or equivalent) from the respective email provider.
14+
15+
Attributes:
16+
to (Union[str, list]): the email address of the recipient - can be a `str` (single recipient) or a `list` (multiple recipients)
17+
**kwargs (Any): Arbitrary keyword arguments - supported keys: `subject`, `from`, `host`, `port`, `tls`, `ssl`, `html`, `login`. Sensible defaults are used for `message` and `subject`, bu they can also be overrided accordingly.
18+
19+
Examples:
20+
>>> import tensorflow as tf
21+
>>> from tf_notify import EmailCallback
22+
>>>
23+
>>> model = tf.keras.Sequential(name="neural-network")
24+
>>> model.add(tf.keras.layers.Dense(1, input_dim=784))
25+
>>> model.compile(
26+
>>> optimizer=tf.keras.optimizers.RMSprop(learning_rate=0.1),
27+
>>> loss="mean_squared_error",
28+
>>> metrics=["mean_absolute_error"],
29+
>>> )
30+
>>>
31+
>>> model.fit(
32+
>>> x_train,
33+
>>> y_train,
34+
>>> batch_size=128,
35+
>>> epochs=2,
36+
>>> verbose=0,
37+
>>> validation_split=0.5,
38+
>>> callbacks=[
39+
>>> EmailCallback(
40+
>>> to="toaddress@yahoo.com",
41+
>>> from_="fromaddress@yahoo.com",
42+
>>> host="smtp.mail.yahoo.com",
43+
>>> port=465,
44+
>>> username="my-cool-username",
45+
>>> password="my-cool-password", # one-time app password
46+
>>> ssl=True,
47+
>>> )
48+
>>> ],
49+
>>> )
50+
51+
**Note**: Any attributes or methods prefixed with _underscores are forming a so called "private" API, and is
52+
for internal use only. They may be changed or removed at anytime.
53+
"""
54+
55+
def __init__(self, to: str, **kwargs: Any):
56+
57+
self.to = to
58+
self.additional_properties = kwargs
59+
self.email = get_notifier("email")
60+
61+
def on_train_end(self, logs=None):
62+
63+
message = self.additional_properties.pop(
64+
"message", f"model <{self.model.name}> has completed its training!"
65+
)
66+
subject = self.additional_properties.pop(
67+
"subject", "New mail from 'tf-notify'!"
68+
)
69+
70+
self.email.notify(
71+
message=message,
72+
to=self.to,
73+
subject=subject,
74+
**self.additional_properties,
75+
)

tf_notify/callbacks/slack.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ class SlackCallback(BaseNotificationCallback):
77
"""
88
A custom tf.callbacks.Callback that provides instant integration with Slack messaging app.
99
10+
Attributes:
11+
webhook_url (str): an Incoming Webhook URL for a given workspace and channel. You can generate one over at: <https://my.slack.com/services/new/incoming-webhook/>
12+
1013
Examples:
1114
>>> import tensorflow as tf
1215
>>> from tf_notify import SlackCallback
1316
>>>
14-
>>> model = tf.keras.Sequential(name='neural-network')
17+
>>> model = tf.keras.Sequential(name="neural-network")
1518
>>> model.add(tf.keras.layers.Dense(1, input_dim=784))
1619
>>> model.compile(
1720
>>> optimizer=tf.keras.optimizers.RMSprop(learning_rate=0.1),
@@ -27,7 +30,7 @@ class SlackCallback(BaseNotificationCallback):
2730
>>> verbose=0,
2831
>>> validation_split=0.5,
2932
>>> callbacks=[
30-
>>> SlackCallback(webhook_url='https://url.to/webhook')
33+
>>> SlackCallback(webhook_url="https://url.to/webhook")
3134
>>> ],
3235
>>> )
3336
@@ -38,7 +41,7 @@ class SlackCallback(BaseNotificationCallback):
3841
def __init__(self, webhook_url: str):
3942

4043
self.webhook_url = webhook_url
41-
self.slack = get_notifier('slack')
44+
self.slack = get_notifier("slack")
4245

4346
def on_train_end(self, logs=None):
4447

0 commit comments

Comments
 (0)