Source code for edri.api.middleware

from __future__ import annotations

from abc import ABC, ABCMeta
from dataclasses import dataclass
from enum import Enum, auto
from typing import Self, Optional, TYPE_CHECKING, Type, Any

if TYPE_CHECKING:
    from edri.dataclass.event import Event
    from edri.api.broker import EventInfo

__all__ = ["Middleware", "MiddlewareControl"]


class MiddlewareMeta(ABCMeta):
    def __init__(cls: "MiddlewareMeta", name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> None:
        super().__init__(name, bases, attrs)

        # Dynamically create the list of methods from the class being created
        required_methods = []
        for base in cls.__mro__:
            for method_name, method in base.__dict__.items():
                if callable(method) and not method_name.startswith('__'):
                    required_methods.append(method_name)

        # Check if at least one method is implemented in the subclass
        if not any(attr in cls.__dict__ for attr in required_methods):
            raise TypeError(
                f"Class '{cls.__name__}' must implement at least one of the following methods: {", ".join(required_methods)}. "
                "Ensure that your class provides an implementation for at least one of these methods."
            )


[docs] @dataclass class MiddlewareControl: """ Encapsulates the outcome of middleware processing with details on the action type, optional data, errors, and an optional replacement event. """
[docs] class ActionType(Enum): """Enum to define middleware processing actions.""" CONTINUE = auto() STOP = auto() REPLACE_EVENT = auto() REDIRECT = auto()
action: ActionType event: Optional[Event] = None
[docs] @classmethod def continue_processing(cls) -> Self: """Creates a MiddlewareControl instance indicating processing should continue.""" return cls(action=cls.ActionType.CONTINUE)
[docs] @classmethod def stop_processing(cls) -> Self: """Creates a MiddlewareControl instance indicating processing should stop.""" return cls(action=cls.ActionType.STOP)
[docs] @classmethod def replace_event(cls, event: Event) -> Self: """ Creates a MiddlewareControl instance that replaces the current event with a new one. Args: event (Event): The event that should replace the current event in processing. Returns: MiddlewareControl: An instance with action set to REPLACE_EVENT. """ return cls(action=cls.ActionType.REPLACE_EVENT, event=event)
[docs] @classmethod def redirect_event(cls, event: Event) -> Self: """ Creates a MiddlewareControl instance that redirects the event (changes request to response or vice versa). Args: event (Event): The event to redirect. Returns: MiddlewareControl: An instance with action set to REDIRECT. """ return cls(action=cls.ActionType.REDIRECT, event=event)
[docs] class Middleware(ABC, metaclass=MiddlewareMeta): """ Abstract base class for middleware components. Middleware classes derived from this must implement at least one of 'process_request' and 'process_response'. """
[docs] @staticmethod def register_events() -> tuple[list[Type[Event]], list[Type[Event]]]: """ Registers events which suppose to be delivered to broker which are before processing handled by middlewares. It supposes to allow user to use custom Events to, for example, authenticate WebSocket connections. In combination with MiddlewareControl.REDIRECT and MiddlewareControl.REPLACE_EVENT is a powerful tool to handle specific situations. Args: Returns: - A tuple of two lists, where each list contains event objects first for request and second for response. """ return [], []
[docs] def process_request(self, event: Event, info: "EventInfo | None") -> MiddlewareControl: """ Method to process the request before it reaches the main application logic. This method should be implemented in a subclass to perform any necessary preprocessing or validation on the request. :param event: The request event to be processed. :param info: Information about the event. :return: A `MiddlewareControl` object indicating whether to continue processing, skip further middleware, or terminate the request/response cycle. """ raise NotImplementedError("Must implement process_request or process_response")
[docs] def process_response(self, event: Event, info: "EventInfo | None" ) -> MiddlewareControl: """ Method to process the response before it is sent to the client. This method should be implemented in a subclass to perform any necessary postprocessing or formatting on the response. :param event: The response event to be processed. :param info: Information about the event. :return: A `MiddlewareControl` object indicating whether to continue processing, skip further middleware, or terminate the request/response cycle. """ raise NotImplementedError("Must implement process_request or process_response")
@classmethod def __getattr__(cls: type[Middleware], name: str) -> bool: prefix = "is_" if name.startswith(prefix): capability_name = f"process_{name[len(prefix):]}" method = cls.__dict__.get(capability_name) return callable(method) raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")