Closed kgasiorowski closed 1 year ago
django
: https://github.com/django/django/tree/main/django/core/mail
sendgrid
: https://docs.sendgrid.com/for-developers/sending-email/quickstart-python
jinja2
: https://frankcorso.dev/email-html-templates-jinja-python.html (not the official documentation, but seems like a good starting point)
^^ yup I have an API key for when we are ready to test but I think first we should come up with a design that lets us swap backends so we can substitute a dummy backend that prints to console. That will lets us test the templating ...
It can be a very simple interface that we can override... probably just a single method abstract that sends a rendered html response. This will let us pass config into the constructor.
We'll also need an object to hold the email credentials we want to send... to avoid extra deps we can use plain class or dataclass for this...
Lastly we'll need some kind of payload object with the data we want to send. It should also be an interface with maybe a method like render
that returns a string. This way we have flexibility and we can create different templates in the applications.
Finally just a stand a lot method send_email
or something that will take the email addresses to send + a payload + backend. So like a controller method.
Super simplfied view of how this should work in an app ...
# in config
...
ADMIN_EMAILS = [
"me@gmail.com",
"you@gmail.com"
]
admin_emails = Emails(ADMIN_EMAILS) # <--- see below
if config.ENVIRONMENT == 'production':
sg_key = os.environ.get('SENGRID_API_KEY')
email_backend = SengridBackend(api_key=sg_key)
if config.ENVIRONMENT == 'local':
email_backend = DummyBackend()
...
# in the app ... ex. in a fastapi exception handler
try:
....
except Exception as err:
content = render_content(request=req, exc=err) # <---- would produce html template with the exc detail and request object
send_emails(admin_emails, backend=email_backend, from_email: str='bpldev@host.com', subject="some error occurred",content=content)
send_emails
controller would need to be run in a threadpool since it blocks, but this is the basic idea.
If we were running this locally the DummyBackend if configured would simply print the email messages to the console...
I'm thinking we might not even need jinja honestly it seems a bit overkill now that i'm looking at this... we can just use just regular html formatted as a multiline string with placeholder {variables}
to inject data in the application. The render_content
method above we can do in fastapi so that we can change up what is needed in the application... it really is just a way to get data into a string.. there is not need to make it fancier then that i think
The key thing is that the content is dynamic at runtime but other parts are lazily initialized. This library actually should not configure any environment variables to make it more flexible for us in a downstream app.
The way I would do this is instead of having a BaseBackend
we'd have a mixin called EmailMessager
. Then "backends" are types that only inherit the action but their constructors vary.
class SendgridBackend(EmailMessager):
def __init__(self, api_key: str ):
self.client = sendgrid.SendGridAPIClient(api_key=api_key)
def send_message(self, messages: Emails) -> int:
... #do stuff with self.client
The Emails class essentially can just become a container for emails to be sent. We can maybe just have it do some validation and hold onto the strings. For the purposes of what we need this to do (send admin emails) this should suffice.
class Emails:
def __init__(self, addresses: List[str] ):
self.emails = [_validate(a) for a in addresses] # <--- some regex or whatever... raise ValueError if fails...
This would just hang around the app in memory like the backend waiting to be used.
send_emails
would then take this method sig:
def send_emails(emails: Emails, backend: EmailMessager, from_email: str, subject: str, content: str) -> None:
...
And we have a bulk emailer more or less...
So, I tried that approach you showed in the first code snippet above, but I ran into problems. Firstly, I'm getting a circular import error because the app uses config
, but config
also makes references back to the backend classes we defined in the app when it checks the environment. How can we get around this?
Secondarily, I'm confused about the ENVIRONMENT
thing. Since this is a standalone library, and its gonna be used in any number of projects, how are we going to get the environment from the .env
? Every project I've checked has its own name for ENVIRONMENT
a la BEIST_ENVIRONMENT
or BPLRPC_ENVIRONMENT
so how can we access that in sendgrid_api
? Will the projects need to add a SENDGRID_ENVIRONMENT
env variable or something like that?
@bsnacks000
I had some comments i think in the PR... we want to do away with config.py
here and just focus on the api... this is more a very small library package rather then an application. I tried to illustrate this above ^^ but maybe its unclear...
In bemadb
for example we'd have:
BEMADB_SENDGRID_API_KEY
under its config module and then it would imported around that way. This lib does not need to read anything from the env... just expose its API for our applications to use as needed.
So here we can basically just ignore it and test by passing stuff into objects at runtime.
We need to write some code which will allow us to send emails.
The
django.core.mail
is a very good starting point in where to look for how to implement this. They include multiple backends, including: a dummy one, one that prints to the console, one that writes to files, and one which actually sends emails. Plenty of useful code there to work with. The dummy one is probably most important, since we need a way to test this stuff without reaching our sendgrid API limits.We'll be using
sendgrid
to actually send the emails. It seems likesendgrid
has a built-in way of handling connections and post requests and such, so hopefully that will work to simplify the code we're working with from django. We will need API keys for that, which is probably a @bsnacks000 thingWe should also bring in
jinja2
and create a local email template to keep things nice and clean.