Flask, Почтовый декоратор

Ниже я покажу пару приятных особенностей Python существенно облегчающих жизнь разработчику

Есть задача отправки почтовых уведомлений об изменении статуса заявки. Решение в лоб подталкивает к созданию подобной фунцкии класса Call:

def send_mail(self):
     if проверка на реальность объекта
          генерация заголовка письма
          генерация текста письма
          try: обработка исключений
              подключени и авторизация на смтп
              отправка письма
              return True

тут видно как образуется минимум четырёх ступенная лесница, что не очень красиво. При этом код отправки сообщения децентрализуется. По-этому можно почесать голову и пойти другим путём, воспользовавшись особенностям Python

Первым делом я предлагаю создать отдельный класс который реализует отправку сообщений. Полученный класс можно включать в список наследуемых классов, тем самым расширяя функционал класса. Например, я определя класс SendMail и примешиваю его к определению модели Call

class Call(Base.Model, SendMail)

Впрочем, подмешивать этот класс можно к любой другой модели, например к модели пользователя (User), для отправки уведомлений о восстановлении пароля. При этом, с точки зрения пользователя класса, хорошо бы просто не париться с такими глупостями как генерация шаблонного текста и заголовка письма, а так же не думать подключение, пускай это будет скрыто во внутренностях объекта. Для этого необходимо, чтобы базовый класс SendMail мог использовать аттрибуты порождённого класса:

msg = MIMEText(self.obj_mail)
msg['Subject'] = self.obj_subject

это подразумевает то, что в объекте производного класса будут использоваться свойства obj_mail и obj_subject и работать это будет так:

# нахожу заявку в базе
call = Call.query.get_or_404(7777)
# отправляю письмо
call.send()

во время вызова метода send() может происходит генерация шаблонного текста и если такое поведение предусмотрено, то письмо отправляется, если объект не готовился к работе с почтой, то метод просто ничего не делает

Для релизации такого поведения необходимо чтобы базовый класс проверял существование созданных объектом атрибутов obj_mail и obj_subject. Сделать это можно с помощью декоратора к методу send

def __precheck__(fun):
        def _wrapper(self):
            if getattr(self, 'obj_subject', None) and getattr(self, 'obj_mail', None):
                fun(self)
        return _wrapper

@__precheck__
def send(self):

Этот декоратор перехватывает вызов метода send объектом производного класса, затем в случае существования среди аттрибутов объекта obj_mail и obj_subject вызывает декорируемую функцию send иначе не делает ничего, то-есть вызов функции send  у ненастроенного объекта вернёт обычное None

Следующим шагом необходимо подготовить аттрибуты obj_mail и obj_subject у производного класса, конечно можно инициализировать эти объекты в конструкторе или каком то специальном методе, но можно определить их как динамические аттрибуты с помощью декоратора @property

def final_email(self):
    return render_template('final_email.html', call=self )
@property
def obj_subject(self):
    if self.id and self.intra:
        return 'ДКМ Технологии, заявка #{0} закрыта'.format(self.intra)
@property
def obj_mail(self):
    if self.id and self.intra:
        return self.final_email()

такое объявление позволит обращаться к функциям obj_mail и obj_subject как к обычным функциям, а значит остаётся свобода имён этих аттрибутов и возможность их диначеского переопределения

Ну и вот вырезки кода поясняющие работу этих механизмов

class SendMail(object):
    def __precheck__(fun):
        def _wrapper(self):
            if getattr(self, 'obj_subject', None) and getattr(self, 'obj_mail', None):
                fun(self)
        return _wrapper
    @__precheck__
    def send(self):
        msg = MIMEText(self.obj_mail)
        msg['Subject'] = self.obj_subject
        msg['From'] = application.config.get('MAIL_LOGIN')
        msg['To'] = ','.join(application.config.get('TARGET_MAIL'))
        try:
            with smtplib.SMTP(application.config.get('MAIL_SERVER')) as s:
                s.login(application.config.get('MAIL_LOGIN'),
                        application.config.get('MAIL_PASSWORD')
                    )
                s.sendmail(application.config.get('MAIL_LOGIN'),
                    application.config.get('TARGET_MAIL'), msg.as_string())

                s.quit()
                return True
        except Exception as error:
            logging.error("ERROR: ", error)

class Call(Base.Model, SendMail):
    __tablename__ = 'calls'
    mysql_character_set = 'utf8'

    id = Column(Integer, primary_key=True, autoincrement=True)
    intra = Column(String(32), nullable=True, doc='№ заявки по базе ')

    def final_email(self):
        if self.id:
            return render_template('final_email.html', call=self )


    def send_final_email(self):
        return self.send()


    @property
    def obj_subject(self):
        if self.id and self.intra:
            return 'ДКМ Технологии, заявка #{0} закрыта'.format(self.intra)

    @property
    def obj_mail(self):
        if self.id and self.intra:
            return self.final_email()

как видите можно расширить поведение путём передачи аргументом имени шаблона для письма, но мне пока это не нужно.

Об авторе
Илья Илья

меня можно найти тут