Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 23, 2023 08:39 am GMT

Providing feature handlers in an elegant way

In a perfect world, you wouldnt have to care about code readability. However, in the context of intense team collaboration, code reuse, and modification, the scripts intelligibility is of the utmost essence. Not only does it save you time, effort, but it also makes your code collaboration-friendly. Thats why we decided to share with you our practical insights for regular devs concerning the handler-provider method for your daily use of feature handlers in the backend.

Lately one of my colleagues has shown me a neat way of providing functional handlers, instead of working with an if-elif-else structure. I liked it so much that not only did I implement it with some pieces of my code, but also decided to share the method with you in this blog post!

Today we will create handlers and a provider that will take care of email sending in a data import process based on two custom model fields:

  • ImportConfiguration.notifications,
  • ImportStatus.status.

You can preview how the fields are defined in the models below:

class ImportConfiguration(Model):    class ImportNotification(TextChoices):        NONE = "none", _("Don't notify me")        FINISHED = "finished", _("Notify me when finished")        ERRORS = "errors", _("Notify me when errors detected")    notifications = CharField(        max_length=8,        choices=ImportNotification.choices,        default=ImportNotification.ERRORS,    )class ImportStatus(Model):    class Status(TextChoices):        AWAITING = "awaiting", _("Awaiting")        IN_PROGRESS = "in_progress", _("In progress")        COMPLETE = "complete", _("Complete")        COMPLETE_WITH_ERRORS = "complete_with_errors", _("Complete with errors")        FAILED = "failed", _("Failed")    status = CharField(        max_length=20,        choices=Status.choices,        default=Status.AWAITING,    )    import_configuration = ForeignKey(ImportConfiguration, on_delete=PROTECT)

The user can set 3 notification settings in the import configuration:

  • NONE, which won't send emails at all for this import,
  • FINISHED, which will send an email when the import has been finished (regardless of the status),
  • ERRORS, which will send an email when import errors occur.

After the import has been finished, you might end up with 3 possible statuses:

  • COMPLETE, when everything goes as planned, with no errors occurring during the import.
  • COMPLETE_WITH_ERRORS, when the import is fine, just some items are corrupted,
  • FAILED, when everything blows up.

Creating the handler

Let's start by declaring an abstract class that will describe our email handlers. I will call it ImportEmailHandler (notice the use of ABC from the abc package). We will also declare the following constant values:

  • _SENDER is the value that will be used as the email sender,
  • _SUBJECT will be the value for the email topic,
  • _TEMPLATE will be the template file for the email,
  • _TEMPLATE_TXT the template for text emails (when someone doesn't support HTML messages).
class ImportEmailHandler(ABC):    _SENDER = "[email protected]"    _SUBJECT: Optional[str] = None    _TEMPLATE: Optional[str] = None    _TEMPLATE_TXT: Optional[str] = None

The handler will provide one public function - send. The goal of this function is to prepare an email message based on the ImportStatus context and then send it to the user.

class ImportEmailHandler(ABC):    ...    @classmethod    def send(cls, import_status: ImportStatus):        message, html_message = cls._prepare_messages(import_status=import_status)        recipients = cls._get_recipients(user=import_status.user)        if recipients:            send_mail(                subject=cls._SUBJECT,                message=message,                html_message=html_message,                from_email=cls._SENDER,                recipient_list=recipients,                fail_silently=True,            )

As you can see, the send function uses two class methods:

  • _prepare_messages will build the messages from the provided template and import context,
  • _get_recipients which will gather a list of emails, to which the message should be delivered.
class ImportEmailHandler(ABC):    ...    @classmethod    def _prepare_message(cls, import_status: ImportStatus) -> Tuple[str, str]:        assert cls._TEMPLATE is not None        assert cls._TEMPLATE_TXT is not None        assert cls._SUBJECT is not None        template = get_template(cls._TEMPLATE_TXT)        html_template = get_template(cls._TEMPLATE)        context = cls._get_context(import_status=import_status)        return template.render(context), html_template.render(context)    @classmethod    def _get_recipients(cls, user: CustomUser) -> List[str]:        # Write your logic to gather email list        return ["[email protected]"]

We also have one more crucial function in the class - the _get_context. This is an abstract method that should be used in any derivative class of the ImportEmailHandler. Its function is to provide context for email templates.

class ImportEmailHandler(ABC):    ...    @classmethod    @abstractmethod    def _get_context(cls, import_status: ImportStatus) -> Dict[str, Any]:        pass

Example of implementation

Let's implement a CompletedEmailHandler to be used when the notifications are set to FINISHED and the status is COMPLETE. Lets see how easy it is to define the handler for this specific case.

class CompletedEmailHandler(ImportEmailHandler):    _SUBJECT = "The import has completed"    _TEMPLATE = "email/completed.html"    _TEMPLATE_TXT = "email/completed.txt"    @classmethod    def _get_context(cls, import_status: ImportStatus) -> Dict[str, Any]:        # The dictionary keys depend on your template        return {            "message": "Your import has completed!",            "status": import_status.status,        }

Provider creation

Now that we have a way to define handlers, we need a provision method suitable to our situation. Let's create an ImportEmailHandlerProvider class! It will have one private variable field called _handlers, as is shown below:

class ImportEmailHandlerProvider:    def __init__(self):        self._handlers: Dict[            ImportConfiguration.ImportNotification,            Dict[ImportStatus.Status, Type[ImportEmailHandler]],        ] = defaultdict(dict)

To register new handlers based on notification and status values, we will create a new public function register, which basically adds handlers to the dictionary.

class ImportEmailHandlerProvider:    ...    def register(        self,        notification: TextChoices,        status: TextChoices,        handler: Type[ImportEmailHandler],    ) -> ImportEmailHandlerProvider:        self._handlers[notification][status] = handler        return self

And another one to retrieve the registered handlers:

class ImportEmailHandlerProvider:    ...    def get(        self,        notification: ImportConfiguration.ImportNotification,        status: ImportStatus.Status,    ) -> Type[ImportEmailHandler]:        try:            return self._handlers[notification][status]        except KeyError as cause:            raise ImportEmailHandlerNotFoundError(                notification=notification, status=status,            ) from cause

However, as you can see, we should raise ImportEmailHandlerNotFoundError when we are not able to provide a handler. This is a custom exception defined in the exceptions.py file that should be handled in your code in some way (depending on what you want to do with this fact).

class ImportEmailHandlerNotFoundError(Exception):    def __init__(        self,        notification: ImportConfiguration.ImportNotification,        status: ImportStatus.Status,    ):        super().__init__(            f"Email import handler for notification '{notification} and "            f"status '{status}' not found."        )

Registering email handlers

Now that we have a class that will store and provide handlers for us - we just need to define which handler should be returned depending on the field parameters. Take a look below - this is how we can do it:

import_email_provider = (    ImportEmailHandlerProvider()    .register(        notification=ImportConfiguration.ImportNotification.FINISHED,        status=ImportStatus.Status.COMPLETE,        handler=CompletedEmailHandler,    )    .register(        notification=ImportConfiguration.ImportNotification.FINISHED,        status=ImportStatus.Status.COMPLETE_WITH_ERRORS,        handler=ErrorEmailHandler,    )    .register(        notification=ImportConfiguration.ImportNotification.FINISHED,        status=ImportStatus.Status.FAILED,        handler=FailedEmailHandler,    )    .register(        notification=ImportConfiguration.ImportNotification.ERRORS,        status=ImportStatus.Status.COMPLETE_WITH_ERRORS,        handler=ErrorEmailHandler,    )    .register(        notification=ImportConfiguration.ImportNotification.ERRORS,        status=ImportStatus.Status.FAILED,        handler=FailedEmailHandler,    ))

The only thing left is to send the email at the end of the import process:

def send_email(import_status: ImportStatus):    # We will start by retrieving the handler    handler = import_email_provider.get(        notification=import_status.import_configuration.notifications,        status=import_status.status,    )    # And now we can send the message!    handler.send(import_status=import_status)

After doing it yourself, I am sure you will be inclined to say that using the described handler-provider method is a great (much more readable) alternative to the standard use of if-elif-else structure, which you can see below:

def send_email(import_status: ImportStatus):    if (        import_status.status == ImportStatus.Status.COMPLETE        and import_status.import_configuration.notifications        == ImportConfiguration.ImportNotification.FINISHED    ):        handler = CompletedEmailHandler    elif (        import_status.status == ImportStatus.Status.COMPLETE        and import_status.import_configuration.notifications        == ImportConfiguration.ImportNotification.FINISHED    ):        handler = ErrorEmailHandler    elif (        import_status.status == ImportStatus.Status.COMPLETE        and import_status.import_configuration.notifications        == ImportConfiguration.ImportNotification.FINISHED    ):        handler = FailedEmailHandler    elif (        import_status.status == ImportStatus.Status.COMPLETE        and import_status.import_configuration.notifications        == ImportConfiguration.ImportNotification.FINISHED    ):        handler = ErrorEmailHandler    elif (        import_status.status == ImportStatus.Status.COMPLETE        and import_status.import_configuration.notifications        == ImportConfiguration.ImportNotification.FINISHED    ):        handler = FailedEmailHandler    else:        raise ImportEmailHandlerNotFoundError    handler.send(import_status=import_status)

That's all Folks!

Up to try it yourself?

Summing up, in this article we showed you a viable alternative to using the if-elif-else structure for providing functional handlers in your Python-based projects. The handler-provider method that we explained here ensures better code intelligibility, making handler and provider creation easier. A way to go, isnt it?


Original Link: https://dev.to/lukzmu/providing-feature-handlers-in-an-elegant-way-47of

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To