-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add SMTPService and Email helper #110
Conversation
e83af8e
to
2ef32d9
Compare
Codecov Report
@@ Coverage Diff @@
## main #110 +/- ##
==========================================
+ Coverage 93.20% 93.25% +0.04%
==========================================
Files 343 346 +3
Lines 26679 26889 +210
==========================================
+ Hits 24866 25075 +209
- Misses 1813 1814 +1
Flags with carried forward coverage won't be shown. Click here to find out more.
|
Codecov Report
@@ Coverage Diff @@
## main #110 +/- ##
==========================================
+ Coverage 93.20% 93.25% +0.04%
==========================================
Files 343 346 +3
Lines 26679 26889 +210
==========================================
+ Hits 24866 25075 +209
- Misses 1813 1814 +1
Flags with carried forward coverage won't be shown. Click here to find out more.
|
Codecov Report
@@ Coverage Diff @@
## main #110 +/- ##
========================================
Coverage 98.45% 98.46%
========================================
Files 366 369 +3
Lines 27023 27233 +210
========================================
+ Hits 26605 26814 +209
- Misses 418 419 +1
Flags with carried forward coverage won't be shown. Click here to find out more.
This change has been scanned for critical changes. Learn more |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a good start.
We might add error handling / retries / metrics later on
Just the Test class name that I'd like to double check. The name is not very descriptive of what it does.
services/tests/test_smtp.py
Outdated
from services.smtp import get_smtp_service | ||
|
||
|
||
class TestStorage(object): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The class name 👀
b5cb008
to
76f2b2c
Compare
76f2b2c
to
8c80f88
Compare
62d15b6
to
ed6fc14
Compare
services/smtp.py
Outdated
_smtp_service = None | ||
|
||
|
||
def get_smtp_service(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand why you are doing this. It might be that the service is not configured, and it makes it possible to re-use connections. I think those are good ideas.
However the pattern we use to instantiate services is to do that directly. It may cause confusion to other devs that try to instantiate the service directly cause they don't know they have to use this function.
My recommendation that you leave the connection aspect in this global / cached situation, and when instantiating the SMTPService()
you use the cached connection
, if available.
Also that you add
@classmethod
def active(self):
return self._conn is not None
for us to know if we can use the SMTP service or not.
(and an extra check in send
to see if the connection is not None either
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i am less concerned with the connection being set to None
and more concerned with the server disconnecting after a long period of inactivity. if joseph takes my suggestion of wrapping the raw connection in something that will automatically reconnect, i don't think we need this active()
method anymore or a check in send()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
despite how long my comments are i think you're very close to merging this. i just talk a lot, the PR is solid lol
services/smtp.py
Outdated
except smtplib.SMTPDataError: | ||
err_msg = "The SMTP server did not accept the data" | ||
|
||
return err_msg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we just log this error message now instead of making the caller do it for us?
i think i'd prefer you create a new SMTPServiceSendFailure
exception and raise it on error. it's more clear/aggressive than returning None
on success and a value on failure, which is the opposite of how functions usually behave. but i think python's type system might be too weak to make me totally happy
(nothing actionable is below this point, just explaining why i don't really like any options here)
when i write error-handling code i keep two questions in mind:
- should the caller care about the outcome of this function?
- if so, how can i force the caller to deal with both success and failure?
on the first point, SMTPService
doesn't have its own retry logic, so retrying is something the caller is responsible for. the caller should care if this function passes or fails. i think this makes sense; the caller is probably a celery task which has a ready-made retry()
function but inside this function we'd need custom logic
but for the second point, python leaves me sad. compare to other languages:
- java makes you declare every type of exception your function can throw. so you either must place your call to
foo()
in atry
/catch
block that handles every exceptionfoo()
can throw, or you must declare your function to throw all the exceptions thatfoo()
can throw (and punt the problem up a stack frame). either way, the compiler forces the caller to make a decision about how to handle the error - C++ and Rust provide ways to require that you handle the return value after you call
foo()
. you can returnResult<Val, Err>
types annotated in this way and the compiler will make the caller explicitly decide how to handle them:
fn foo() -> Result<T, E>;
fn bar1() {
foo(); // fails to build because return value is not assigned to anything
}
fn bar2() {
let result = foo(); // fails because the result variable is never actually used
}
fn bar3() {
let result = foo();
if result.is_err() { // passes because we're actually using the result variable
schedule_retry();
}
}
fn bar4() {
let _ = foo(); // passes because _ is a magic "i am intentionally ignoring this" identifier
}
python can make you handle neither return values nor exceptions. an uncaught exception will make the caller handle the error after they ship a bug into production, but python can't save them from the bug in the first place like other languages can
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see if you can add a test case for the "reconnect" logic:
mocker.patch.object(SMTPService.connection, 'noop', side_effect=SMTPServerDisconnected("foo"))
mock_fn = mocker.patch.object(smtp, 'make_connection')
smtp.send(...)
assert mock_fn.has_calls(...) # or whatever
approving with nits, great work seeing this through!
log.warning( | ||
"The SMTP server did not accept the data", extra=self.extra_dict | ||
) | ||
raise SMTPServiceError("The SMTP server did not accept the data") from exc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think you could save some lines with the err_msg
approach you used earlier
if not SMTPService.connection:
// this one is returning right now which is inconsistent with raising the exception in the other error cases
err_msg = "Connection was not initialized"
try:
....
except smtplib.SMTPReceipientsRefused:
err_msg = "..."
except ...:
err_msg = "..."
if err_msg:
log.warning(err_msg)
raise SMTPServiceError(err_msg)
b16d903
to
b932b3f
Compare
services/smtp.py
Outdated
self.try_starttls() | ||
|
||
if self.username and self.password: | ||
self.try_login() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be nice to get rid of this duplicated code. it's already in make_connection()
, could we just use that here too? maybe add the if SMTPService.connection is None
stuff inside make_connection()
too
not a blocking comment, and sorry if it was like this before and i missed commenting on it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it was not there, I added these methods in one of my recent commits, the reason I'm not using make_connection here is that here we are creating the SMTP
object, but in make_connection we're assuming that this step has already been done and we're just running connect
again on that existing object
service.connection.starttls.assert_called_with(context=m.return_value) | ||
service.connection.login.assert_called_with("test_username", "test_password") | ||
|
||
def test_idempotentconnectionection(self, mocker, mock_configuration): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test_idempotent_connection
* Email helper class that wraps EmailMessage * SMTPService class that creates connection to SMTP server using config options in services.smtp * send method on SMTPService that takes Email as an arg to send an email using the SMTP connection Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
Signed-off-by: joseph-sentry <[email protected]>
b932b3f
to
105ed31
Compare
Signed-off-by: joseph-sentry <[email protected]>
Email helper class that wraps EmailMessage
SMTPService class that creates connection to SMTP server using config options in services.smtp
send method on SMTPService that takes Email as an arg to send an email using the SMTP connection