Module FuncNotify.NotifyMethods

Parent class for all of FuncNotify useability All NotifyMethods are child of this and use send_start_message, send_end_message, and send_error_message functions

Expand source code
"""Parent class for all of FuncNotify useability
All NotifyMethods are child of this and use send_start_message, 
send_end_message, and send_error_message functions
"""
import os
import time
import traceback
import inspect

import logging
import logging.handlers

import socket
import collections

from abc import ABCMeta, abstractmethod


DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
    

class FactoryRegistry(ABCMeta):
    _REGISTRY = {} 
    
    def __new__(cls, clsname, bases, attrs):
        newclass = super().__new__(cls, clsname, bases, attrs)
        if not inspect.isabstract(newclass):  # Removes abstract methods from registry
            cls._REGISTRY[newclass.__name__.replace("Method", "")] = newclass
        return newclass
    
    @classmethod
    def get_cls_registry(cls)->dict:
        """Registers every class created in a dictionary, creating automated
        factory methoods
        
        Returns:
            dict: Takes a string of type (Class name without method) and returns NotifyObj
        """        
        return dict(cls._REGISTRY)
    
    
class NotifyMethods(metaclass=FactoryRegistry):
    """Abstract class for the methods of notifying the user, \
    handles the messages and logger for error checking
    """    
    # Tracking and testing, intended to in case one needs to check functions ran
    _buffer = collections.deque([], maxlen=5) # Tracks last five for error checking, 
    
    __slots__ = ("__environ_dict", "_error")
    
    logger=None
    log_method_dict={}
        
    _messageDict = {"Start": ["Function: `{0}` called...",
                              "Machine Name: {machine}",
                              "Start Time: {1}"],
                    
                    "End":   ["Function: `{0}` completed",
                              "Machine Name: {machine}",
                              "Finish Time: {1}",
                              "Total Time: {2:.2f}"],
                    
                    "Error": ["Function: `{0}` failed due to a {1}",
                              "Exception Reason: {2}"
                              "Fail Time Stamp: {3}",
                              "Machine Name: {machine}",
                              "Fail Traceback: {4}"],
                    
                    "Custom": ["{0}"],
                    }
    
    def __init__(self, environ: dict=None, mute: bool=False, use_log: bool=False, *args, **kwargs):
        self.__environ_dict = environ if isinstance(environ, dict) else {}
        NotifyMethods.set_mute(mute)
        
        try:  
            NotifyMethods.logger_init(self.__environ_dict, self.__environ_dict, use_log, *args, **kwargs) 
            # Why do I have to declare the __environ_dict twice? I have no idea TODO someone smarter help
            self._set_credentials(*args, **kwargs)
            self._error=None # Always default to notify user

        except Exception as ex:
            NotifyMethods.log(status="ERROR", METHOD=self.__class__.__name__, 
                                message="[CREDENTIALS] Connection to setting up notifications \
                                        interupted, double check env variables")
            NotifyMethods.log(status="ERROR", METHOD=self.__class__.__name__, 
                              message=f"[CREDENTIALS] {ex}") 
            self._error=CredentialError(self, ex) # If error with credentials
        
        NotifyMethods._add_buffer(self)

    @property     
    def environ_dict(self):
        """Wanted to hide environment variables but still be able to test
        Returns:
            bool: Whether environ_dict contains anything"""        
        return not not self.__environ_dict
    
    def _type_or_env(self, val, env_variable: str, type_: type=str)->str:
        """Checks if inputted value is of the type `type_`, default to string, otherwise \
        searches environment for that variable. If not found, doesn't notify ussers

        Args:
            val (any): Input, should always be a string but if not will search environment
            type_ (type): the type too coompare to 
            env_variable (str): environment variable name

        Returns:
            type_: important information used by apis
        Raises:
            KeyError: Raises if environment variable not found in name, this will set `self._error` \
            to that exception so it can be accessed
        """           
        return val if isinstance(val, type_) else self.__environ_dict[env_variable] 
    
    @classmethod
    def _add_buffer(cls, NotifyObject):
        """Adds each object to a pseudo cyclical buffer that holds 5 objects that
        can be checked when you grab the buffer
        """ 
        if isinstance(NotifyObject._error, Exception): 
            NotifyObject=NotifyObject._error
        cls._buffer.append(NotifyObject)
        
    @classmethod
    def get_buffer(cls):
        """Buffer holding previous NotifyMethods to be able to interact with

        Returns:
            deque: Holds last 5 objects
        """
        return cls._buffer
    
    @classmethod
    def set_mute(cls, mute: bool=False):
        """Mutes the send of messages for the entire class

        Args:
            mute (bool, optional): whether to enable/disable messages for a period of time. Defaults to False.
        """        
        cls._mute = mute if isinstance(mute, bool) else False
    

    @classmethod
    def logger_init(cls, environ: dict, log: bool=False, buffer: int=65536, logger_path: str=None, *args, **kwargs):
        """Initializes a logger to tract messages sent and errors (not errors outside of FuncNotify) that arise from sending the message.
        
        Args:
            environ (dict): current environment variables
            log (bool, optional): Whether to log the files]. Defaults to False.
            buffer (int, optional): Size of each log file. Defaults to 65536 (2**16).
            logger_path (str, optional): path to logger. Defaults to None.
        """      
        if (environ.get("LOG") or log or logger_path) and cls.logger is None: # Uses existing logger if it existss
            
            if logger_path:
                path=logger_path
            else:
                path = environ.get("LOGGER_PATH", "")
                path = path if path else os.getcwd() # If env variable but not defined is empty sets path to cwd
                
            if not os.path.isdir(os.path.join(path, "logs")):
                os.mkdir("logs")

            import __main__ # Necessary for naming, setting up print formatting
            logger_name = __main__.__file__.split('/')[-1].split('.')[0]

            cls.logger = logging.getLogger(logger_name)
            cls.logger.setLevel(logging.DEBUG)

            logger_console_format = "[%(levelname)s]: %(message)s"
            console_handler = logging.StreamHandler()
            console_handler.setLevel(logging.WARNING)
            console_handler.setFormatter(logging.Formatter(logger_console_format))
            cls.logger.addHandler(console_handler)

            logger_file_format = "[%(levelname)s] - %(asctime)s - %(name)s - : %(message)s in %(pathname)s:%(lineno)d"
            file_handler = logging.handlers.RotatingFileHandler(filename=f"{path}/logs/{logger_name}.log",
                                                                maxBytes=int(environ.get("FILE_SIZE", buffer)), 
                                                                backupCount=1)
            file_handler.setLevel(logging.WARNING)
            file_handler.setFormatter(logging.Formatter(logger_file_format))
            cls.logger.addHandler(file_handler)

            # Dictionary houses all logging methods
            logger_strings = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "OFF"]
            logger_levels = range(logging.DEBUG, logging.CRITICAL + 11, 10)
            logger_funcs = [cls.logger.debug, cls.logger.info, cls.logger.warning, cls.logger.error, cls.logger.critical]
            
            cls.log_method_dict = dict(zip(logger_strings, logger_funcs))
            cls.log_level_dict = dict(zip(logger_strings, logger_levels))
            
        elif not (environ.get("LOG") or log or logger_path) and environ:
            cls.logger_off()
            


    # Logger suite, functions that control logging functinos that run
    @classmethod
    def set_logger(cls, level: int=None, level_string: str=None):
        """Determines whether the loggger should pay atention to. The default \
        level is `Warning` and calling this function will set it to `Debug`.

        Args:
            level (int, optional): level to set log to level. Mututally exclusive with level_string. 
            Defaults to logging.DEBUG.
            level_string (str, optional): str representation to set log level to. \
            Must be all capitalized letters. Mututally exclusive with level.  
            Defaults to "DEBUG".
        """        
        if cls.logger is None:
            NotifyMethods.logger_init(log=True)
        
        if level is not None and level_string is not None:
            raise ValueError("`level` and `level_string` are mutually exclusive variables")
        else:
            lvl = max(level if isinstance(level, int) else -1, cls.log_level_dict.get(level_string, -1))
            lvl = lvl if lvl != -1 else logging.DEBUG
        cls.logger.setLevel(lvl)
    @classmethod
    def logger_off(cls):
        """Turn off logger by setting the logger value so high nothing triggers it
        """        
        cls.set_logger(logging.CRITICAL+1)
    @classmethod
    def _format_log(cls, status: str, METHOD: str, message: str, *args, **kwargs):
        ret_messsage = f"[{METHOD=}] Message = {message}"
        return ret_messsage, {'exc_info': status>logging.INFO} 
    
    @classmethod
    def log(cls, status: str="DEBUG", *args, **kwargs):
        """Logs the current event, one can pass multiple argugments too
        
        Args:
            status (str, optional): logging level. Defaults to "DEBUG".
        """        
        if cls.logger:
            log_message, kwdict = cls._format_log(cls.log_level_dict.get(status, logging.ERROR), *args, **kwargs)
            cls.log_method_dict.get(status, 
                                    lambda *args, **kwargs: [
                                        cls.logger.error(*args, **kwargs),
                                        cls.logger.error("Logger method not found, using [ERROR]"),]
                                    )(log_message, **kwdict)
    
    @abstractmethod
    def _set_credentials(self, *args, **kwargs)->None:
        """Sets up object with environment variables
        """        
        pass
    
    @abstractmethod
    def _send_message(self, message: str)->None: 
        """Interacts with the respective platforms apis, the prior 3 all call this functioon to send the message
        """        
        pass

    # Suite of funcitons sends and formats messages for each different method. These guys help format each message for each of the instances
    def send_start_MSG(self, func): 
        self._send_MSG_base(formatList=[func.__name__, time.strftime(DATE_FORMAT, time.localtime())], machine=socket.gethostname(),
                            type_="Start")
    def send_end_MSG(self, func, diff: float): 
        self._send_MSG_base(formatList=[func.__name__, time.strftime(DATE_FORMAT, time.localtime()), diff], machine=socket.gethostname(), 
                            type_="End")
    def send_error_MSG(self, func, ex: Exception): 
        self._send_MSG_base(formatList=[func.__name__, type(ex), str(ex), time.strftime(DATE_FORMAT, time.localtime()), traceback.format_exc()], machine=socket.gethostname(),
                            type_="Error")
    def send_custom_MSG(self, MSG: str): 
        """Send custom messages, kind of an easter egg and will require a bit of custom code ot set up
        Args:
            MSG (str): Any valid string
        """        
        self._send_MSG_base(formatList=[MSG], type_="Custom")
    
    def _format_message(self, formatList: list, type_: str="Error", *args, **kwargs):
        return '\n'.join(NotifyMethods._messageDict[type_]).format(*formatList, *args, **kwargs) + self._addon(type_=type_)
    

    def _addon(self, type_: str="Error")->str:
        """Pseudo-abstsract method, sometimess will add emojis and other fun messages
        that are platform specific. Not necessary to implement but you can for personalization!
        """        
        return ""
    
    def _send_MSG_base(self, *args, **kwargs)->None:
        """All functions begin by calling send_MSG_base and depending on the status of that functioon, it'll be sent or
        an error will be logged if the initial credentials aren't valid

        Args:
            MSG (str): Current MSG to be sent.  
        """ 
        MSG = self._format_message(*args, **kwargs)
        
        if not NotifyMethods._mute:       
            if self._error:
                NotifyMethods.log(status="ERROR", METHOD=self.__class__.__name__, 
                                  message=f"[ERROR] {self._error} \n[Message] {MSG}")
                return
            
            try:
                self._send_message(MSG)
                NotifyMethods.log(status="DEBUG", METHOD=self.__class__.__name__, 
                                  message=MSG)

            except Exception as ex:
                self._error=MessageSendError(self, ex)
                NotifyMethods.log(status="ERROR", METHOD=self.__class__.__name__, 
                                  message=f"[Error] {self._error} \n[Message] {MSG}")
        else:
            NotifyMethods.log(status="INFO", METHOD=self.__class__.__name__, 
                                  message=f"[Message] {MSG} \n[Muted] True")
                
                
class CredentialError(Exception):
    """Errrors occuring while setting up the credentials"""    
    __slots__=("NotifyObject", "error")
    def __init__(self, NotifyObject: NotifyMethods, error: Exception):
        self.NotifyObject=NotifyObject
        """NotifyMethods object where something went wrong"""        
        self.error=error 
        """The Error with the NotifyMethods object"""        
        super().__init__(self.__str__())
    
    def __str__(self):
        return f"The following exception occurred with the credentials of using {self.NotifyObject.__class__.__name__} \n" \
               f"[Error] {self.error} \n" \
               f"[Fix] Check all credentials are strings and are accurate, check the type hints, and env variables"
        
class MessageSendError(Exception):
    """Errors that occur when sending the message and are caught then"""    
    __slots__=("NotifyObject", "error")
    def __init__(self, NotifyObject: NotifyMethods, error: Exception):
        self.NotifyObject=NotifyObject
        """"NotifyMethods object where something went wrong""" 
        self.error=error 
        """The Error with the NotifyMethods object"""  
        super().__init__(self.__str__())
    
    def __str__(self):
        return f"The following exception occurred while sending the messagge with the method {self.NotifyObject.__class__.__name__} \n"\
               f"[Error] {self.error} \n" \
               f"[Fix] This is an error with the respective platform's API, ensure the credentials for are valid and you have access," \
               f"check env variables, and ensure that all the types are correct. This is likely an issue with your implementation."
        
        

Classes

class CredentialError (NotifyObject: NotifyMethods, error: Exception)

Errrors occuring while setting up the credentials

Expand source code
class CredentialError(Exception):
    """Errrors occuring while setting up the credentials"""    
    __slots__=("NotifyObject", "error")
    def __init__(self, NotifyObject: NotifyMethods, error: Exception):
        self.NotifyObject=NotifyObject
        """NotifyMethods object where something went wrong"""        
        self.error=error 
        """The Error with the NotifyMethods object"""        
        super().__init__(self.__str__())
    
    def __str__(self):
        return f"The following exception occurred with the credentials of using {self.NotifyObject.__class__.__name__} \n" \
               f"[Error] {self.error} \n" \
               f"[Fix] Check all credentials are strings and are accurate, check the type hints, and env variables"

Ancestors

  • builtins.Exception
  • builtins.BaseException

Instance variables

var NotifyObject

NotifyMethods object where something went wrong

var error

The Error with the NotifyMethods object

class FactoryRegistry (*args, **kwargs)

Metaclass for defining Abstract Base Classes (ABCs).

Use this metaclass to create an ABC. An ABC can be subclassed directly, and then acts as a mix-in class. You can also register unrelated concrete classes (even built-in classes) and unrelated ABCs as 'virtual subclasses' – these and their descendants will be considered subclasses of the registering ABC by the built-in issubclass() function, but the registering ABC won't show up in their MRO (Method Resolution Order) nor will method implementations defined by the registering ABC be callable (not even via super()).

Expand source code
class FactoryRegistry(ABCMeta):
    _REGISTRY = {} 
    
    def __new__(cls, clsname, bases, attrs):
        newclass = super().__new__(cls, clsname, bases, attrs)
        if not inspect.isabstract(newclass):  # Removes abstract methods from registry
            cls._REGISTRY[newclass.__name__.replace("Method", "")] = newclass
        return newclass
    
    @classmethod
    def get_cls_registry(cls)->dict:
        """Registers every class created in a dictionary, creating automated
        factory methoods
        
        Returns:
            dict: Takes a string of type (Class name without method) and returns NotifyObj
        """        
        return dict(cls._REGISTRY)

Ancestors

  • abc.ABCMeta
  • builtins.type

Static methods

def get_cls_registry() ‑> dict

Registers every class created in a dictionary, creating automated factory methoods

Returns

dict
Takes a string of type (Class name without method) and returns NotifyObj
Expand source code
@classmethod
def get_cls_registry(cls)->dict:
    """Registers every class created in a dictionary, creating automated
    factory methoods
    
    Returns:
        dict: Takes a string of type (Class name without method) and returns NotifyObj
    """        
    return dict(cls._REGISTRY)
class MessageSendError (NotifyObject: NotifyMethods, error: Exception)

Errors that occur when sending the message and are caught then

Expand source code
class MessageSendError(Exception):
    """Errors that occur when sending the message and are caught then"""    
    __slots__=("NotifyObject", "error")
    def __init__(self, NotifyObject: NotifyMethods, error: Exception):
        self.NotifyObject=NotifyObject
        """"NotifyMethods object where something went wrong""" 
        self.error=error 
        """The Error with the NotifyMethods object"""  
        super().__init__(self.__str__())
    
    def __str__(self):
        return f"The following exception occurred while sending the messagge with the method {self.NotifyObject.__class__.__name__} \n"\
               f"[Error] {self.error} \n" \
               f"[Fix] This is an error with the respective platform's API, ensure the credentials for are valid and you have access," \
               f"check env variables, and ensure that all the types are correct. This is likely an issue with your implementation."

Ancestors

  • builtins.Exception
  • builtins.BaseException

Instance variables

var NotifyObject

"NotifyMethods object where something went wrong

var error

The Error with the NotifyMethods object

class NotifyMethods (environ: dict = None, mute: bool = False, use_log: bool = False, *args, **kwargs)

Abstract class for the methods of notifying the user, handles the messages and logger for error checking

Expand source code
class NotifyMethods(metaclass=FactoryRegistry):
    """Abstract class for the methods of notifying the user, \
    handles the messages and logger for error checking
    """    
    # Tracking and testing, intended to in case one needs to check functions ran
    _buffer = collections.deque([], maxlen=5) # Tracks last five for error checking, 
    
    __slots__ = ("__environ_dict", "_error")
    
    logger=None
    log_method_dict={}
        
    _messageDict = {"Start": ["Function: `{0}` called...",
                              "Machine Name: {machine}",
                              "Start Time: {1}"],
                    
                    "End":   ["Function: `{0}` completed",
                              "Machine Name: {machine}",
                              "Finish Time: {1}",
                              "Total Time: {2:.2f}"],
                    
                    "Error": ["Function: `{0}` failed due to a {1}",
                              "Exception Reason: {2}"
                              "Fail Time Stamp: {3}",
                              "Machine Name: {machine}",
                              "Fail Traceback: {4}"],
                    
                    "Custom": ["{0}"],
                    }
    
    def __init__(self, environ: dict=None, mute: bool=False, use_log: bool=False, *args, **kwargs):
        self.__environ_dict = environ if isinstance(environ, dict) else {}
        NotifyMethods.set_mute(mute)
        
        try:  
            NotifyMethods.logger_init(self.__environ_dict, self.__environ_dict, use_log, *args, **kwargs) 
            # Why do I have to declare the __environ_dict twice? I have no idea TODO someone smarter help
            self._set_credentials(*args, **kwargs)
            self._error=None # Always default to notify user

        except Exception as ex:
            NotifyMethods.log(status="ERROR", METHOD=self.__class__.__name__, 
                                message="[CREDENTIALS] Connection to setting up notifications \
                                        interupted, double check env variables")
            NotifyMethods.log(status="ERROR", METHOD=self.__class__.__name__, 
                              message=f"[CREDENTIALS] {ex}") 
            self._error=CredentialError(self, ex) # If error with credentials
        
        NotifyMethods._add_buffer(self)

    @property     
    def environ_dict(self):
        """Wanted to hide environment variables but still be able to test
        Returns:
            bool: Whether environ_dict contains anything"""        
        return not not self.__environ_dict
    
    def _type_or_env(self, val, env_variable: str, type_: type=str)->str:
        """Checks if inputted value is of the type `type_`, default to string, otherwise \
        searches environment for that variable. If not found, doesn't notify ussers

        Args:
            val (any): Input, should always be a string but if not will search environment
            type_ (type): the type too coompare to 
            env_variable (str): environment variable name

        Returns:
            type_: important information used by apis
        Raises:
            KeyError: Raises if environment variable not found in name, this will set `self._error` \
            to that exception so it can be accessed
        """           
        return val if isinstance(val, type_) else self.__environ_dict[env_variable] 
    
    @classmethod
    def _add_buffer(cls, NotifyObject):
        """Adds each object to a pseudo cyclical buffer that holds 5 objects that
        can be checked when you grab the buffer
        """ 
        if isinstance(NotifyObject._error, Exception): 
            NotifyObject=NotifyObject._error
        cls._buffer.append(NotifyObject)
        
    @classmethod
    def get_buffer(cls):
        """Buffer holding previous NotifyMethods to be able to interact with

        Returns:
            deque: Holds last 5 objects
        """
        return cls._buffer
    
    @classmethod
    def set_mute(cls, mute: bool=False):
        """Mutes the send of messages for the entire class

        Args:
            mute (bool, optional): whether to enable/disable messages for a period of time. Defaults to False.
        """        
        cls._mute = mute if isinstance(mute, bool) else False
    

    @classmethod
    def logger_init(cls, environ: dict, log: bool=False, buffer: int=65536, logger_path: str=None, *args, **kwargs):
        """Initializes a logger to tract messages sent and errors (not errors outside of FuncNotify) that arise from sending the message.
        
        Args:
            environ (dict): current environment variables
            log (bool, optional): Whether to log the files]. Defaults to False.
            buffer (int, optional): Size of each log file. Defaults to 65536 (2**16).
            logger_path (str, optional): path to logger. Defaults to None.
        """      
        if (environ.get("LOG") or log or logger_path) and cls.logger is None: # Uses existing logger if it existss
            
            if logger_path:
                path=logger_path
            else:
                path = environ.get("LOGGER_PATH", "")
                path = path if path else os.getcwd() # If env variable but not defined is empty sets path to cwd
                
            if not os.path.isdir(os.path.join(path, "logs")):
                os.mkdir("logs")

            import __main__ # Necessary for naming, setting up print formatting
            logger_name = __main__.__file__.split('/')[-1].split('.')[0]

            cls.logger = logging.getLogger(logger_name)
            cls.logger.setLevel(logging.DEBUG)

            logger_console_format = "[%(levelname)s]: %(message)s"
            console_handler = logging.StreamHandler()
            console_handler.setLevel(logging.WARNING)
            console_handler.setFormatter(logging.Formatter(logger_console_format))
            cls.logger.addHandler(console_handler)

            logger_file_format = "[%(levelname)s] - %(asctime)s - %(name)s - : %(message)s in %(pathname)s:%(lineno)d"
            file_handler = logging.handlers.RotatingFileHandler(filename=f"{path}/logs/{logger_name}.log",
                                                                maxBytes=int(environ.get("FILE_SIZE", buffer)), 
                                                                backupCount=1)
            file_handler.setLevel(logging.WARNING)
            file_handler.setFormatter(logging.Formatter(logger_file_format))
            cls.logger.addHandler(file_handler)

            # Dictionary houses all logging methods
            logger_strings = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "OFF"]
            logger_levels = range(logging.DEBUG, logging.CRITICAL + 11, 10)
            logger_funcs = [cls.logger.debug, cls.logger.info, cls.logger.warning, cls.logger.error, cls.logger.critical]
            
            cls.log_method_dict = dict(zip(logger_strings, logger_funcs))
            cls.log_level_dict = dict(zip(logger_strings, logger_levels))
            
        elif not (environ.get("LOG") or log or logger_path) and environ:
            cls.logger_off()
            


    # Logger suite, functions that control logging functinos that run
    @classmethod
    def set_logger(cls, level: int=None, level_string: str=None):
        """Determines whether the loggger should pay atention to. The default \
        level is `Warning` and calling this function will set it to `Debug`.

        Args:
            level (int, optional): level to set log to level. Mututally exclusive with level_string. 
            Defaults to logging.DEBUG.
            level_string (str, optional): str representation to set log level to. \
            Must be all capitalized letters. Mututally exclusive with level.  
            Defaults to "DEBUG".
        """        
        if cls.logger is None:
            NotifyMethods.logger_init(log=True)
        
        if level is not None and level_string is not None:
            raise ValueError("`level` and `level_string` are mutually exclusive variables")
        else:
            lvl = max(level if isinstance(level, int) else -1, cls.log_level_dict.get(level_string, -1))
            lvl = lvl if lvl != -1 else logging.DEBUG
        cls.logger.setLevel(lvl)
    @classmethod
    def logger_off(cls):
        """Turn off logger by setting the logger value so high nothing triggers it
        """        
        cls.set_logger(logging.CRITICAL+1)
    @classmethod
    def _format_log(cls, status: str, METHOD: str, message: str, *args, **kwargs):
        ret_messsage = f"[{METHOD=}] Message = {message}"
        return ret_messsage, {'exc_info': status>logging.INFO} 
    
    @classmethod
    def log(cls, status: str="DEBUG", *args, **kwargs):
        """Logs the current event, one can pass multiple argugments too
        
        Args:
            status (str, optional): logging level. Defaults to "DEBUG".
        """        
        if cls.logger:
            log_message, kwdict = cls._format_log(cls.log_level_dict.get(status, logging.ERROR), *args, **kwargs)
            cls.log_method_dict.get(status, 
                                    lambda *args, **kwargs: [
                                        cls.logger.error(*args, **kwargs),
                                        cls.logger.error("Logger method not found, using [ERROR]"),]
                                    )(log_message, **kwdict)
    
    @abstractmethod
    def _set_credentials(self, *args, **kwargs)->None:
        """Sets up object with environment variables
        """        
        pass
    
    @abstractmethod
    def _send_message(self, message: str)->None: 
        """Interacts with the respective platforms apis, the prior 3 all call this functioon to send the message
        """        
        pass

    # Suite of funcitons sends and formats messages for each different method. These guys help format each message for each of the instances
    def send_start_MSG(self, func): 
        self._send_MSG_base(formatList=[func.__name__, time.strftime(DATE_FORMAT, time.localtime())], machine=socket.gethostname(),
                            type_="Start")
    def send_end_MSG(self, func, diff: float): 
        self._send_MSG_base(formatList=[func.__name__, time.strftime(DATE_FORMAT, time.localtime()), diff], machine=socket.gethostname(), 
                            type_="End")
    def send_error_MSG(self, func, ex: Exception): 
        self._send_MSG_base(formatList=[func.__name__, type(ex), str(ex), time.strftime(DATE_FORMAT, time.localtime()), traceback.format_exc()], machine=socket.gethostname(),
                            type_="Error")
    def send_custom_MSG(self, MSG: str): 
        """Send custom messages, kind of an easter egg and will require a bit of custom code ot set up
        Args:
            MSG (str): Any valid string
        """        
        self._send_MSG_base(formatList=[MSG], type_="Custom")
    
    def _format_message(self, formatList: list, type_: str="Error", *args, **kwargs):
        return '\n'.join(NotifyMethods._messageDict[type_]).format(*formatList, *args, **kwargs) + self._addon(type_=type_)
    

    def _addon(self, type_: str="Error")->str:
        """Pseudo-abstsract method, sometimess will add emojis and other fun messages
        that are platform specific. Not necessary to implement but you can for personalization!
        """        
        return ""
    
    def _send_MSG_base(self, *args, **kwargs)->None:
        """All functions begin by calling send_MSG_base and depending on the status of that functioon, it'll be sent or
        an error will be logged if the initial credentials aren't valid

        Args:
            MSG (str): Current MSG to be sent.  
        """ 
        MSG = self._format_message(*args, **kwargs)
        
        if not NotifyMethods._mute:       
            if self._error:
                NotifyMethods.log(status="ERROR", METHOD=self.__class__.__name__, 
                                  message=f"[ERROR] {self._error} \n[Message] {MSG}")
                return
            
            try:
                self._send_message(MSG)
                NotifyMethods.log(status="DEBUG", METHOD=self.__class__.__name__, 
                                  message=MSG)

            except Exception as ex:
                self._error=MessageSendError(self, ex)
                NotifyMethods.log(status="ERROR", METHOD=self.__class__.__name__, 
                                  message=f"[Error] {self._error} \n[Message] {MSG}")
        else:
            NotifyMethods.log(status="INFO", METHOD=self.__class__.__name__, 
                                  message=f"[Message] {MSG} \n[Muted] True")

Subclasses

Class variables

var log_method_dict
var logger

Static methods

def get_buffer()

Buffer holding previous NotifyMethods to be able to interact with

Returns

deque
Holds last 5 objects
Expand source code
@classmethod
def get_buffer(cls):
    """Buffer holding previous NotifyMethods to be able to interact with

    Returns:
        deque: Holds last 5 objects
    """
    return cls._buffer
def log(status: str = 'DEBUG', *args, **kwargs)

Logs the current event, one can pass multiple argugments too

Args

status : str, optional
logging level. Defaults to "DEBUG".
Expand source code
@classmethod
def log(cls, status: str="DEBUG", *args, **kwargs):
    """Logs the current event, one can pass multiple argugments too
    
    Args:
        status (str, optional): logging level. Defaults to "DEBUG".
    """        
    if cls.logger:
        log_message, kwdict = cls._format_log(cls.log_level_dict.get(status, logging.ERROR), *args, **kwargs)
        cls.log_method_dict.get(status, 
                                lambda *args, **kwargs: [
                                    cls.logger.error(*args, **kwargs),
                                    cls.logger.error("Logger method not found, using [ERROR]"),]
                                )(log_message, **kwdict)
def logger_init(environ: dict, log: bool = False, buffer: int = 65536, logger_path: str = None, *args, **kwargs)

Initializes a logger to tract messages sent and errors (not errors outside of FuncNotify) that arise from sending the message.

Args

environ : dict
current environment variables
log : bool, optional
Whether to log the files]. Defaults to False.
buffer : int, optional
Size of each log file. Defaults to 65536 (2**16).
logger_path : str, optional
path to logger. Defaults to None.
Expand source code
@classmethod
def logger_init(cls, environ: dict, log: bool=False, buffer: int=65536, logger_path: str=None, *args, **kwargs):
    """Initializes a logger to tract messages sent and errors (not errors outside of FuncNotify) that arise from sending the message.
    
    Args:
        environ (dict): current environment variables
        log (bool, optional): Whether to log the files]. Defaults to False.
        buffer (int, optional): Size of each log file. Defaults to 65536 (2**16).
        logger_path (str, optional): path to logger. Defaults to None.
    """      
    if (environ.get("LOG") or log or logger_path) and cls.logger is None: # Uses existing logger if it existss
        
        if logger_path:
            path=logger_path
        else:
            path = environ.get("LOGGER_PATH", "")
            path = path if path else os.getcwd() # If env variable but not defined is empty sets path to cwd
            
        if not os.path.isdir(os.path.join(path, "logs")):
            os.mkdir("logs")

        import __main__ # Necessary for naming, setting up print formatting
        logger_name = __main__.__file__.split('/')[-1].split('.')[0]

        cls.logger = logging.getLogger(logger_name)
        cls.logger.setLevel(logging.DEBUG)

        logger_console_format = "[%(levelname)s]: %(message)s"
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.WARNING)
        console_handler.setFormatter(logging.Formatter(logger_console_format))
        cls.logger.addHandler(console_handler)

        logger_file_format = "[%(levelname)s] - %(asctime)s - %(name)s - : %(message)s in %(pathname)s:%(lineno)d"
        file_handler = logging.handlers.RotatingFileHandler(filename=f"{path}/logs/{logger_name}.log",
                                                            maxBytes=int(environ.get("FILE_SIZE", buffer)), 
                                                            backupCount=1)
        file_handler.setLevel(logging.WARNING)
        file_handler.setFormatter(logging.Formatter(logger_file_format))
        cls.logger.addHandler(file_handler)

        # Dictionary houses all logging methods
        logger_strings = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "OFF"]
        logger_levels = range(logging.DEBUG, logging.CRITICAL + 11, 10)
        logger_funcs = [cls.logger.debug, cls.logger.info, cls.logger.warning, cls.logger.error, cls.logger.critical]
        
        cls.log_method_dict = dict(zip(logger_strings, logger_funcs))
        cls.log_level_dict = dict(zip(logger_strings, logger_levels))
        
    elif not (environ.get("LOG") or log or logger_path) and environ:
        cls.logger_off()
def logger_off()

Turn off logger by setting the logger value so high nothing triggers it

Expand source code
@classmethod
def logger_off(cls):
    """Turn off logger by setting the logger value so high nothing triggers it
    """        
    cls.set_logger(logging.CRITICAL+1)
def set_logger(level: int = None, level_string: str = None)

Determines whether the loggger should pay atention to. The default level is Warning and calling this function will set it to Debug.

Args

level : int, optional
level to set log to level. Mututally exclusive with level_string.
Defaults to logging.DEBUG.
level_string : str, optional
str representation to set log level to. Must be all capitalized letters. Mututally exclusive with level.

Defaults to "DEBUG".

Expand source code
@classmethod
def set_logger(cls, level: int=None, level_string: str=None):
    """Determines whether the loggger should pay atention to. The default \
    level is `Warning` and calling this function will set it to `Debug`.

    Args:
        level (int, optional): level to set log to level. Mututally exclusive with level_string. 
        Defaults to logging.DEBUG.
        level_string (str, optional): str representation to set log level to. \
        Must be all capitalized letters. Mututally exclusive with level.  
        Defaults to "DEBUG".
    """        
    if cls.logger is None:
        NotifyMethods.logger_init(log=True)
    
    if level is not None and level_string is not None:
        raise ValueError("`level` and `level_string` are mutually exclusive variables")
    else:
        lvl = max(level if isinstance(level, int) else -1, cls.log_level_dict.get(level_string, -1))
        lvl = lvl if lvl != -1 else logging.DEBUG
    cls.logger.setLevel(lvl)
def set_mute(mute: bool = False)

Mutes the send of messages for the entire class

Args

mute : bool, optional
whether to enable/disable messages for a period of time. Defaults to False.
Expand source code
@classmethod
def set_mute(cls, mute: bool=False):
    """Mutes the send of messages for the entire class

    Args:
        mute (bool, optional): whether to enable/disable messages for a period of time. Defaults to False.
    """        
    cls._mute = mute if isinstance(mute, bool) else False

Instance variables

var environ_dict

Wanted to hide environment variables but still be able to test

Returns

bool
Whether environ_dict contains anything
Expand source code
@property     
def environ_dict(self):
    """Wanted to hide environment variables but still be able to test
    Returns:
        bool: Whether environ_dict contains anything"""        
    return not not self.__environ_dict

Methods

def send_custom_MSG(self, MSG: str)

Send custom messages, kind of an easter egg and will require a bit of custom code ot set up

Args

MSG : str
Any valid string
Expand source code
def send_custom_MSG(self, MSG: str): 
    """Send custom messages, kind of an easter egg and will require a bit of custom code ot set up
    Args:
        MSG (str): Any valid string
    """        
    self._send_MSG_base(formatList=[MSG], type_="Custom")
def send_end_MSG(self, func, diff: float)
Expand source code
def send_end_MSG(self, func, diff: float): 
    self._send_MSG_base(formatList=[func.__name__, time.strftime(DATE_FORMAT, time.localtime()), diff], machine=socket.gethostname(), 
                        type_="End")
def send_error_MSG(self, func, ex: Exception)
Expand source code
def send_error_MSG(self, func, ex: Exception): 
    self._send_MSG_base(formatList=[func.__name__, type(ex), str(ex), time.strftime(DATE_FORMAT, time.localtime()), traceback.format_exc()], machine=socket.gethostname(),
                        type_="Error")
def send_start_MSG(self, func)
Expand source code
def send_start_MSG(self, func): 
    self._send_MSG_base(formatList=[func.__name__, time.strftime(DATE_FORMAT, time.localtime())], machine=socket.gethostname(),
                        type_="Start")