> Also knowing where it is called now doesn't necessarily mean it won't get called in other places in the future.

Yeah, this is exactly why I want to guarantee that this function always returns a value rather than having each caller have to handle exceptions individually.  Basically, "here's what you call to get the report content, guaranteed, even if it's just a message about an error, so that you can use it in a user-facing context without having to worry about catching any exceptions and determining whether retrying might work, etc."

Based on the 12 error notifications this month in send_report() from random exceptions in the report code which resulted in a scheduled report not being sent at all, it's clear that at least Exception needs to be caught.

> It's also totally theoretically possible that some interrupt exception could get called and swallowed via celery or
django.

I don't really have that good of an understanding of gunicorn, celery, and gevent, so let me know if I'm getting something wrong.  The scenarios I see are:

- naughty code called inside of try: raises an exception that isn't a subclass of Exception.  We want to return an error message and then continue onward rather than completely fail to send the scheduled report.
- gunicorn calls the kill method on a gevent.Greenlet because it thinks it's taking too long (I made that up, but it seems right), which causes a GreenletExit to be thrown "into" the greenlet.  We want to to return an error message and then finish the request, right? (which shouldn't take too much longer, and I'm sure gunicorn will just kill the greenlet outright if it really wants to or something)
- celery throws SystemTerminate into one of its tasks because somehow a cold shutdown was ordered instead of a warm shutdown.  It seems to still make sense to catch that and finish the task, which shouldn't take too much longer, and of course whatever is killing celery can always completely kill it if it wants.
- something or other sends a SIGINT to the gunicorn or celery process (whether by Ctrl-C or some other method) and it passes it on to the worker thread/greenlet/whatever (is that how that works? I have no clue).  It seems to still make sense to catch the KeyboardInterrupt and let the task finish, and let whatever wants to kill  the process kill it for real if it really wants to.

I would like to know what other values besides GreenletExit and SystemTerminate would occur in those scenarios, because it would be nice to know what exceptions you can expect to have to handle to prevent a feature from having a rather bad failure mode.  In fact, is there any possible exception for which it wouldn't make sense to simply return an error message and then finish up processing the other reports in the email before exiting?

If you meant something else by that, I didn't get it.

Also, this should clearly still be logging any caught exceptions.  I'll add a notify_exception call.