Source code for dmf.alerts.decorator

from typing import Callable, Optional
from datetime import datetime
from pathlib import Path


from .alerts import send_alert

__all__ = ["alert", "DoNotAlert"]

class DoNotAlert(Exception):
    """Exception raised to prevent an alert from being sent."""
    pass


[docs] def alert( func: Optional[Callable] = None, *, output: bool = False, input: bool = False, max_length: int = 100, disable: bool = False, ) -> Callable: """ Decorator that sends an alert after the execution of the decorated function. The decorator can be applied with or without parentheses, allowing flexibility in how you use it to notify about function execution. It supports logging of input parameters, output values, and the duration of the function execution. :param func: Optional; The function to be decorated. :param output: If True, include the function's output in the alert message. If the output is a Path object, the file will be sent in the message. :param input: If True, log the input parameters (args and kwargs). If a list of argument names is provided, only those arguments will be logged. :param max_length: Maximum length of the output string to be logged. :return: The decorator function. **Examples**: Will notify when the execution of this function finishes, with information about the start time and duration:: @alert def hello(name): return f"Hello {name}!" Here, the input arguments are also included in the notification:: @alert(input=True) def hello(name): return f"Hello {name}!" Here, only the specified input argument ('name') will be included in the notification:: @alert(input=["name"]) def hello(name, surname): return f"Hello {name} {surname}!" Here, the output will be included in the message:: @alert(output=True) def hello(name, surname): return f"Hello {name} {surname}!" If the output is a Path, the file will be sent in the message:: @alert(output=True) def hello(name): print(f"Hello {name}") return Path("/path/to/{name}.pdf") """ # Quick disable of the alert decorator if disable: if func is None: return lambda func: func return func if func is None: # Used as @alert(output=True, input=True) with parentheses def decorator(inner_func: Callable) -> Callable: return _alert_decorator( inner_func, output=output, input=input, max_length=max_length ) return decorator else: # Used as @alert without parentheses return _alert_decorator(func, output=output, input=input, max_length=max_length)
def _alert_decorator( func: Callable, output: bool, input: bool, max_length: int, max_attach_size: int = 10 * 1024 * 1024, ) -> Callable: def wrapper(*args, **kwargs): start_time = datetime.now() attachment = None params = { ":calendar: Start Time": start_time.strftime("%Y-%m-%d %H:%M:%S"), ":bell: End Time": "", ":stopwatch: Duration": "", } if input: input_str = _input_to_str(args, kwargs, input, max_length) if input_str: params[":inbox_tray: Input"] = input_str try: result = func(*args, **kwargs) end_time = datetime.now() message = f"Execution of function *{func.__name__}* finished successfully." params[":bell: End Time"] = end_time.strftime("%Y-%m-%d %H:%M:%S") params[":stopwatch: Duration"] = str(end_time - start_time).split(".")[0] if output: params[":outbox_tray: Output"] = f"_{_to_str(result)}_" # Check is a file path and not too large if ( isinstance(result, Path) and result.exists() and result.stat().st_size < max_attach_size ): attachment = result send_alert( text=message, level="success", params=params, attachment=attachment ) return result except DoNotAlert: # Do not send an alert if the DoNotAlert exception is raised return result except Exception as e: end_time = datetime.now() message = f"Execution of function *{func.__name__}* failed with an error." params[":bell: End Time"] = end_time.strftime("%Y-%m-%d %H:%M:%S") params[":stopwatch: Duration"] = str(end_time - start_time).split(".")[0] params[f":exclamation: {e.__class__.__name__}"] = f"_'{str(e)}'_" send_alert(text=message, level="error", params=params) raise e return wrapper def _to_str(object, max_length: int = 100) -> str: text = str(object) if max_length and len(text) > max_length: text = text[: max_length - 3] + "..." return text def _input_to_str(args, kwargs, input, max_length: int = 100) -> str: input_str = "" sep = "\n" + 10 * " " + "• " if input is True: for i, arg in enumerate(args, 1): input_str += f"{sep}*arg{i}*: _{_to_str(arg, max_length)}_" for key, value in kwargs.items(): if input is True or key in input: input_str += f"{sep}*{key}*: _{_to_str(value, max_length)}_" return input_str # Assign the DoNotAlert exception to the alert decorator setattr(alert, "DoNotAlert", DoNotAlert)