<reponame>RyanCPeters/backup<gh_stars>0

"""
This is a general purpose utility file for setting up logger output that's good for
quick visual scans of a program's diagnostic data.

By: <NAME>
Email: <EMAIL>
"""

# builtin imports
import sys
from sys import stdout as SYS_STDOUT
from typing import Union, Iterable
import logging
import inspect
from logging import Formatter
from datetime import datetime as dt
from pathlib import Path
import os

# custom project imports
from twindb_backup.util.pathing_defs import project_root_folder, proj_name



########################################################################################################################
# Determining if we are running on Windows, then set the global variable USE_COLORS accordingly.
########################################################################################################################
IS_WINDOWS = os.name=="nt"

########################################################################################################################
# Determining if we are running in debug mode, then set the global variable DEBUGGING accordingly.
#
# DEBUGGING should now be imported to the project's root __init__.py file for consistent
# access across the project
########################################################################################################################
try:
    # Pycharm IDE specific implementation detail
    # This detects if we launched into debug mode using the Pycharm IDE
    import pydevd
    DEBUGGING = True
except ImportError:
    # Pycharm debugger isn't running, so let's check if the code was called
    # from a terminal with the `-d` flag set (`-d` tells python interpreter to run in debug mode)
    DEBUGGING = sys.flags.debug == 1  # note that sys.flags is a namedtuple object
if DEBUGGING:
    logging.basicConfig(level=logging.DEBUG)

# the following "table" of _data was taken from the official python docs for
# python version 3.6.8 on May 5, 2019
# found here: https://docs.python.org/3.6/library/logging.html
# see that link for the full description of the available attributes
#
# Attribute name	    Format
# args	                You shouldn’t need to format this yourself.
# asctime	            %(asctime)s
# created	            %(created)f
# exc_info	            You shouldn’t need to format this yourself.
# filename	            %(filename)s
# funcName	            %(funcName)s
# levelname	            %(levelname)s
# levelno	            %(levelno)s
# lineno	            %(lineno)d
# message	            %(message)s
# module	            %(module)s
# msecs	                %(msecs)d
# msg	                You shouldn’t need to format this yourself.
# name	                %(name)s
# pathname	            %(pathname)s
# process	            %(process)d
# processName	        %(processName)s
# relativeCreated	    %(relativeCreated)d
# thread	            %(thread)d
# threadName	        %(threadName)s

# Level	    Numeric value
# CRITICAL  50
# ERROR     40
# WARNING   30
# INFO      20
# DEBUG     10
# NOTSET    0

LOGGER_COLLECTION = {}

ESC_SEQ = "\033["
HIGH_PRI_LOGGER_HEADER_COLOR_PREFIX = ESC_SEQ + "1;4;7;{}m"
LOW_PRI_LOGGER_HEADER_COLOR_PREFIX = ESC_SEQ + "1;4;{}m"
LOGGER_MSG_COLOR = ESC_SEQ + "1;{}m"
LOGGER_MSG_COLOR_INVERTED = ESC_SEQ + "1;7;{}m"
NOTSET_ID = 90  # charcoal
DBG_ID = 35  # purple
INFO_ID = 32  # green -- might be just my eyes, but green(32) and yellow(33) look almost identical
WARN_ID = 36  # dull-cyan
EXC_ID = 30  # white-dull
ERR_ID = 95  # bright-purple
CRIT_ID = 91  # bright-red
RESET = ESC_SEQ + "0m"
color_formatting_dict = {
    "reset":RESET,
    "format_keys": {
        "esc_seq": ESC_SEQ,
        "hi_pri_prefix": HIGH_PRI_LOGGER_HEADER_COLOR_PREFIX,
        "lo_pri_prefix": LOW_PRI_LOGGER_HEADER_COLOR_PREFIX,
        "logger_msg_color":LOGGER_MSG_COLOR,
        "logger_msg_color_inverted":LOGGER_MSG_COLOR_INVERTED,
    },
    "color_codes":{
        "not_set":NOTSET_ID,
        "debug":DBG_ID,
        "info":INFO_ID,
        "warning":WARN_ID,
        "exception":EXC_ID,
        "error":ERR_ID,
        "critical":CRIT_ID,
        "white_dull":30,
        "red_dull":31,
        "green_dull":32,
        "yellow_dull":33,
        "purple_dull":35,
        "blue_dull":34,
        "cyan_dull":36,
        "gray_dull":37,
        "gray_bright":90,
        "red_bright":91,
        "green_bright":92,
        "yellow_bright":93,
        "blue_bright":94,
        "purple_bright":95,
        "cyan_bright":96,
        "white_bright":97,
    }
}
# logging.addLevelName(35,"EXCEPTION")
log_id_map = {logging.ERROR: ERR_ID, logging.INFO: INFO_ID, logging.WARNING: WARN_ID,
              logging.DEBUG: DBG_ID, logging.NOTSET: NOTSET_ID,logging.CRITICAL:CRIT_ID, 35:EXC_ID}
LEVELS_STR2INT = {
    "NOTSET": logging.NOTSET,
    "DEBUG": logging.DEBUG,
    "INFO": logging.INFO,
    "WARNING": logging.WARNING,
    # "EXCEPTION": 35,
    "ERROR": logging.ERROR,
    "CRITICAL": logging.CRITICAL}

LEVELS_INT2STR = {v:k for k,v in LEVELS_STR2INT.items()}

logging_target_files = {
    "DEBUG":"**/logging_output/debug/{}.log",
    "INFO":"**/logging_output/info/{}.log",
    "WARNING":"**/logging_output/warning/{}.log",
    "ERROR":"**/logging_output/error/{}.log",
    "CRITICAL":"**/logging_output/critical/{}.log",
    "LEVEL SPECIAL":{
        # for future cases where the code requires a clearly named custom implementation
        # place that implementation here and special handling can be developed for it.
    }
}

if IS_WINDOWS:
    LOW_PRI_FORMATTER = "%(asctime)s - {child_name} // called from: %(module)s.%(funcName)s(...) - %(lineno)d:" \
                        "{stack_info}%(message)s"
    HIGH_PRI_FORMATTER = "%(asctime)s - {child_name} // called from: %(module)s.%(funcName)s(...) - %(lineno)d:" \
                         " ::> %(message)s{stack_info}"
else:
    LOW_PRI_FORMATTER = "{header}%(asctime)s - {child_name} // called from: %(module)s.%(funcName)s(...) - %(lineno)d:{reset}{msg_style}" \
                        "{stack_info}%(message)s{reset}"
    HIGH_PRI_FORMATTER = "{header}%(asctime)s - {child_name} // called from: %(module)s.%(funcName)s(...) - %(lineno)d:{reset}{msg_style}" \
                         " ::> %(message)s{stack_info}{reset}"

class CallStackFormatter(logging.Formatter):
    """
    This custom formatter class was taken directly from:
        https://stackoverflow.com/q/54747730/7412747
    """

    def formatStack(self, _ = None) -> str:
        stack = inspect.stack()[::-1]
        stack_names = (inspect.getmodulename(stack[0].filename),
                       *(frame.function
                         for frame
                         in stack[1:-9]))
        return '::'.join(stack_names)

    def format(self, record):
        record.message = record.getMessage()
        record.stack_info = self.formatStack()
        if self.usesTime():
            record.asctime = self.formatTime(record, self.datefmt)
        s = self.formatMessage(record)
        if record.exc_info:
            # Cache the traceback text to avoid converting it multiple times
            # (it's constant anyway)
            if not record.exc_text:
                record.exc_text = self.formatException(record.exc_info)
        if record.exc_text:
            if s[-1:] != "\n":
                s = s + "\n"
            s = s + record.exc_text
        return s


def _build_default_formatter(level:int,child_name:str,colr_id:int, formatter_class=None)->Union[logging.Formatter, CallStackFormatter]:
    if formatter_class is None:
        formatter_class = logging.Formatter
    if isinstance(formatter_class,CallStackFormatter):
        stack_info = "\n\t[ %(levelname)-5s ] [ %(stack_info)s ]"
    else:
        stack_info = ""
    if level < logging.WARNING:  # logging.WARNING == 30
        fstr = LOW_PRI_FORMATTER
        header = LOW_PRI_LOGGER_HEADER_COLOR_PREFIX.format(colr_id)
    else:
        fstr = HIGH_PRI_FORMATTER
        header = HIGH_PRI_LOGGER_HEADER_COLOR_PREFIX.format(colr_id)
    return formatter_class(
        fstr.format(
            child_name=child_name,
            stack_info=stack_info,
            header=header,
            msg_style=LOGGER_MSG_COLOR.format(colr_id),
            reset=RESET),
        "%H:%M:%S")



def _ensured_configure_handler(formatter:logging.Formatter, level:int,
                               handler_delegate:Union[logging.StreamHandler,Iterable[logging.StreamHandler]],
                               handler_kwargs:dict)->Union[logging.Handler,list[logging.Handler]]:
    if handler_delegate==logging.StreamHandler:
        handler_delegate = [handler_delegate]
    handlers = []
    for delegate in handler_delegate:
        handler_kwargs.setdefault("stream", SYS_STDOUT)
        handler = delegate(**handler_kwargs)
        handler.setLevel(level)
        handler.setFormatter(formatter)
        handlers.append(handler)
    if len(handlers)==1:
        return handlers[0]
    return handlers


def _logger_setup(logger: logging.Logger,
                  header_label: str,
                  formatter: Formatter = None,
                  level: int = logging.INFO,
                  level_normalized: int = logging.INFO,
                  handler_delegate=Union[logging.StreamHandler,Iterable[logging.StreamHandler]],
                  **handler_kwargs) ->logging.Logger:
    """A convenience function for creating well named loggers with optional custom formatter.

    This function will implement an already defined formatter for you if the formatter param is
    None. For an example of a good general use logger see the example code at the bottom of this
    file.

    SPECIAL NOTE:
        Although this function's signature allows the caller to pass virtually any logging.Handler
        subclass into the handler_delegate parameter, I've only tested this functionality against
        the logging.StreamHandler class. If you wish to use it for others you will likely encounter
        bugs. But if you are up to the task it seems like it could potentially be a helpful tool for
        allowing the instantiation of a wide array of utility loggers under a single interface.

    :param header_label:
    :param logger: an initialized logger object that needs to have its formatter and optinoal handlers set.

    :param child_name:  The auxiliary logger you wish to create to handle some specific task. The
                        logger which maps to this child will set its level, handlers, and formatters
                        according to your inputs, allowing you to specify many different loggers to
                        manage data output in a form that suites you.
    :type child_name:   A string

    :param formatter:   An optional parameter that specifies the manner in which you wish to format
                        the available logging-event-_data as well as how you wish to present the
                        message _data for your log events. The default formatter will take one of
                        two styles, based the given `level`.

                        For level==logging.INFO (20) and bellow:
                            log messages will be presented in two parts.
                                * First, a header line that's formatted to be bold, and underlined,
                                  that gives the time-stamp for when the log was submitted, the
                                  child_name, the model and function and line number from which the
                                  log-message was originated
                                * Followed by an indented new-line where the log-message will be
                                  be printed. The message
                        For level==logging.INFO+1 (21) and above:
                            log messages will be presented in two parts.
                                * First, a header line that's formatted to be bold, and underlined,
                                  that gives the time-stamp for when the log was submitted, the
                                  child_name, the model and function and line number from which the
                                  log-message was originated
                                * Followed, on the same line as the header, the log-message. This
                                  distinction, as opposed to the indented new-line in lower level
                                  messaged, is done because it is often the case the when higher
                                  level messages occur, there are very many of them. Forcing each
                                  one to then be a multi-line message actually makes it much harder
                                  to visually parse.
                                * Special note: In order to aid in automated parsing of these
                                  log-messages, the header details and log message will be seperated
                                  by the following character key:
                                        `::>`

    :type formatter:    an instance of logging.Formatter

    :param handler_delegate: An optional parameter that Specifies the type of handler you want to
                             associate to the logger instance that's mapped to the
                             root_name.child_name you've passed in. The handler will be set up
                             inside of this function, this parameter simply allows you to indicate
                             the way you wish to have your output handled.
                             (E.G. to std_out, std_err, or some file output stream)
    :type handler_delegate:  This should be a delegate function of your desired handler's
                             constructor, DEFAULT=logging.StreamHandler

    :param level:   Specifies the desired logging level of the resulting logger.
                    DEFAULT=logging.DEBUG
    :type level:    An int, must be in the range of [0,0xffffffff]

    :type handler_kwargs: Any additional keywords that should be passed into the construction of the
                          handler. These are necessary for the instantiation of handlers that will
                          output to anything other than sys.std_out, and sys.std_err.

    :return:    a reference to the logger instance that's mapped to the input naming scheme of
                <root_name>.<child_name >
    :rtype: logging.Logger
    """
    logger.propagate = False
    level_normalized = level if level_normalized is None else level_normalized
    try:
        colr_id = log_id_map[level_normalized]
    except KeyError:
        # level_normalized isn't in our custom log_id_map
        if level_normalized in logging._levelToName:
            # but it has been registered with the logging library
            LEVELS_STR2INT[logging._levelToName[level_normalized]] = level_normalized
        for lvl,color_code in sorted(log_id_map.items(),key=lambda tpl:tpl[0]):
            if lvl<=level_normalized:
                colr_id = color_code
            else:
                break
        else:
            colr_id = log_id_map["NOTSET"]
        log_id_map[level_normalized] = colr_id
    if formatter is None:
        formatter = _build_default_formatter(level,header_label,colr_id,formatter)
    logger.addHandler(_ensured_configure_handler(formatter, level, handler_delegate, handler_kwargs))
    return logger


def get_logger(root_name: str,
               child_name: str = None,
               header_label: str = None,
               formatter: Union[Formatter,str] = None,
               level: Union[int, str] = None,
               logger_registration_dict: dict = None,
               handler_delegate:Union[logging.StreamHandler,Iterable[logging.StreamHandler]]=logging.StreamHandler,
               do_file_output:bool=False,
               **handler_kwargs)->logging.Logger:
    global LOGGER_COLLECTION
    if logger_registration_dict is None:
        logger_registration_dict = LOGGER_COLLECTION
    if isinstance(formatter,str):
        if "callstackformatter" == formatter.lower():
            formatter = CallStackFormatter
    level = level if level is not None else (logging.DEBUG if DEBUGGING else logging.NOTSET)
    if isinstance(level, str):
        level = LEVELS_STR2INT.get(level.upper(), logging.NOTSET)
    if not (logging.NOTSET<=level<=logging.CRITICAL):
        _logger_setup_internal_warning_logger(f"NOTICE: The level passed to the logger_setup function was not in the range of [{logging.NOTSET},{logging.CRITICAL}] and is going to be clipped to those bounds")
    level_normalized = max(level, logging.NOTSET)
    while level_normalized>logging.NOTSET:
        name = logging._levelToName.get(level_normalized,None)
        if name is not None:
            break
        level_normalized -= 1
    if child_name is not None:
        child_name = child_name if child_name else logging.getLevelName(level_normalized)
        header_label = f"{root_name}.{child_name or header_label}"
        logger = logging.getLogger(root_name).getChild(child_name)
    else:
        header_label = root_name
        logger = logging.getLogger(root_name)
    _logger_setup_kwargs = dict(logger=logger, header_label=header_label, formatter=formatter, level=level,
                                level_normalized=level_normalized, handler_delegate=handler_delegate, **handler_kwargs)
    if logger.name not in logger_registration_dict:
        logger_registration_dict[logger.name] = _logger_setup(**_logger_setup_kwargs)
    return logger_registration_dict[logger.name]


def get_logging_file_target(level:str or int, fname:str or Path, just_first:bool=True):
    def do_backup(p):
        if p.exists():
            backup = p.with_name(p.name + ".backup")
            with open(backup, "a", encoding="utf-8") as f:
                f.write(p.read_text("utf-8"))
        with open(p, "w", encoding="utf-8") as f:
            f.write(f"{level} -- START: {dt.now()}\n")
        return p
    if isinstance(level,int):
        level = LEVELS_INT2STR.get(level,logging.getLevelName(level))
    level = level.upper()
    if not fname:
        raise ValueError("Received an empty file name when deriving output file path for logging")
    for k,v in logging_target_files.items():
        if isinstance(v,str):
            path = project_root_folder.joinpath(v[3:]).parent
            path.mkdir(parents=True,exist_ok=True)
        else:
            for sk,sv in v.items():
                path = project_root_folder.joinpath(f"special/{sv[3:]}").parent
                path.mkdir(parents=True, exist_ok=True)
    if "SPECIAL" in level:
        raise NotImplementedError(f"Handling of special case logging targets has not be implemented yet.\n\ttarget fname: {fname}")
    if fname.endswith(".log"):
        fname = fname.strip(".log")
    target = logging_target_files[level].format(fname)
    ret = []
    path_gen = project_root_folder.rglob(target)
    if just_first:
        try:
            return [do_backup(next(path_gen))]
        except GeneratorExit:
            # file does not already exist, so we need to create it.
            return [do_backup(project_root_folder.joinpath(target[3:]))]
    for p in path_gen:
        ret.append(do_backup(p))
    if not ret:
        # no paths match search pattern, so we need to create an appropriate path
        ret.append(do_backup(project_root_folder.joinpath(target[3:])))
    return ret

_logger_setup_internal_warning_logger = get_logger(__name__, "logger_setup_warning", level="WARNING",logger_registration_dict={})
_logger_setup_internal_warning_logger = _logger_setup_internal_warning_logger.warning


# project_root = project_root_folder.name # gets the name of the project's root directory
project_root = proj_name # gets the name of the project's root directory
import_warnings_logger = get_logger(project_root,__name__+": import warnings",level="WARNING").warning
root_info_logger = get_logger(project_root,__name__+": INFO log",level="INFO")


# third party logging supression
if DEBUGGING:
    for loggername in "numba,azure,azure.core".split(","):
        try:
            logging.getLogger(loggername).setLevel(logging.WARNING)
        except BaseException:
            pass

"""
This is a general purpose utility file for setting up logger output that's good for
quick visual scans of a program's diagnostic data.

By: <NAME>
Email: <EMAIL>
"""

# builtin imports
import sys
from sys import stdout as SYS_STDOUT
from typing import Union, Iterable
import logging
import inspect
from logging import Formatter
from datetime import datetime as dt
from pathlib import Path
import os

# custom project imports
from twindb_backup.util.pathing_defs import project_root_folder, proj_name



########################################################################################################################
# Determining if we are running on Windows, then set the global variable USE_COLORS accordingly.
########################################################################################################################
IS_WINDOWS = os.name=="nt"

########################################################################################################################
# Determining if we are running in debug mode, then set the global variable DEBUGGING accordingly.
#
# DEBUGGING should now be imported to the project's root __init__.py file for consistent
# access across the project
########################################################################################################################
try:
    # Pycharm IDE specific implementation detail
    # This detects if we launched into debug mode using the Pycharm IDE
    import pydevd
    DEBUGGING = True
except ImportError:
    # Pycharm debugger isn't running, so let's check if the code was called
    # from a terminal with the `-d` flag set (`-d` tells python interpreter to run in debug mode)
    DEBUGGING = sys.flags.debug == 1  # note that sys.flags is a namedtuple object
if DEBUGGING:
    logging.basicConfig(level=logging.DEBUG)

# the following "table" of _data was taken from the official python docs for
# python version 3.6.8 on May 5, 2019
# found here: https://docs.python.org/3.6/library/logging.html
# see that link for the full description of the available attributes
#
# Attribute name	    Format
# args	                You shouldn’t need to format this yourself.
# asctime	            %(asctime)s
# created	            %(created)f
# exc_info	            You shouldn’t need to format this yourself.
# filename	            %(filename)s
# funcName	            %(funcName)s
# levelname	            %(levelname)s
# levelno	            %(levelno)s
# lineno	            %(lineno)d
# message	            %(message)s
# module	            %(module)s
# msecs	                %(msecs)d
# msg	                You shouldn’t need to format this yourself.
# name	                %(name)s
# pathname	            %(pathname)s
# process	            %(process)d
# processName	        %(processName)s
# relativeCreated	    %(relativeCreated)d
# thread	            %(thread)d
# threadName	        %(threadName)s

# Level	    Numeric value
# CRITICAL  50
# ERROR     40
# WARNING   30
# INFO      20
# DEBUG     10
# NOTSET    0

LOGGER_COLLECTION = {}

ESC_SEQ = "\033["
HIGH_PRI_LOGGER_HEADER_COLOR_PREFIX = ESC_SEQ + "1;4;7;{}m"
LOW_PRI_LOGGER_HEADER_COLOR_PREFIX = ESC_SEQ + "1;4;{}m"
LOGGER_MSG_COLOR = ESC_SEQ + "1;{}m"
LOGGER_MSG_COLOR_INVERTED = ESC_SEQ + "1;7;{}m"
NOTSET_ID = 90  # charcoal
DBG_ID = 35  # purple
INFO_ID = 32  # green -- might be just my eyes, but green(32) and yellow(33) look almost identical
WARN_ID = 36  # dull-cyan
EXC_ID = 30  # white-dull
ERR_ID = 95  # bright-purple
CRIT_ID = 91  # bright-red
RESET = ESC_SEQ + "0m"
color_formatting_dict = {
    "reset":RESET,
    "format_keys": {
        "esc_seq": ESC_SEQ,
        "hi_pri_prefix": HIGH_PRI_LOGGER_HEADER_COLOR_PREFIX,
        "lo_pri_prefix": LOW_PRI_LOGGER_HEADER_COLOR_PREFIX,
        "logger_msg_color":LOGGER_MSG_COLOR,
        "logger_msg_color_inverted":LOGGER_MSG_COLOR_INVERTED,
    },
    "color_codes":{
        "not_set":NOTSET_ID,
        "debug":DBG_ID,
        "info":INFO_ID,
        "warning":WARN_ID,
        "exception":EXC_ID,
        "error":ERR_ID,
        "critical":CRIT_ID,
        "white_dull":30,
        "red_dull":31,
        "green_dull":32,
        "yellow_dull":33,
        "purple_dull":35,
        "blue_dull":34,
        "cyan_dull":36,
        "gray_dull":37,
        "gray_bright":90,
        "red_bright":91,
        "green_bright":92,
        "yellow_bright":93,
        "blue_bright":94,
        "purple_bright":95,
        "cyan_bright":96,
        "white_bright":97,
    }
}
# logging.addLevelName(35,"EXCEPTION")
log_id_map = {logging.ERROR: ERR_ID, logging.INFO: INFO_ID, logging.WARNING: WARN_ID,
              logging.DEBUG: DBG_ID, logging.NOTSET: NOTSET_ID,logging.CRITICAL:CRIT_ID, 35:EXC_ID}
LEVELS_STR2INT = {
    "NOTSET": logging.NOTSET,
    "DEBUG": logging.DEBUG,
    "INFO": logging.INFO,
    "WARNING": logging.WARNING,
    # "EXCEPTION": 35,
    "ERROR": logging.ERROR,
    "CRITICAL": logging.CRITICAL}

LEVELS_INT2STR = {v:k for k,v in LEVELS_STR2INT.items()}

logging_target_files = {
    "DEBUG":"**/logging_output/debug/{}.log",
    "INFO":"**/logging_output/info/{}.log",
    "WARNING":"**/logging_output/warning/{}.log",
    "ERROR":"**/logging_output/error/{}.log",
    "CRITICAL":"**/logging_output/critical/{}.log",
    "LEVEL SPECIAL":{
        # for future cases where the code requires a clearly named custom implementation
        # place that implementation here and special handling can be developed for it.
    }
}

if IS_WINDOWS:
    LOW_PRI_FORMATTER = "%(asctime)s - {child_name} // called from: %(module)s.%(funcName)s(...) - %(lineno)d:" \
                        "{stack_info}%(message)s"
    HIGH_PRI_FORMATTER = "%(asctime)s - {child_name} // called from: %(module)s.%(funcName)s(...) - %(lineno)d:" \
                         " ::> %(message)s{stack_info}"
else:
    LOW_PRI_FORMATTER = "{header}%(asctime)s - {child_name} // called from: %(module)s.%(funcName)s(...) - %(lineno)d:{reset}{msg_style}" \
                        "{stack_info}%(message)s{reset}"
    HIGH_PRI_FORMATTER = "{header}%(asctime)s - {child_name} // called from: %(module)s.%(funcName)s(...) - %(lineno)d:{reset}{msg_style}" \
                         " ::> %(message)s{stack_info}{reset}"

class CallStackFormatter(logging.Formatter):
    """
    This custom formatter class was taken directly from:
        https://stackoverflow.com/q/54747730/7412747
    """

    def formatStack(self, _ = None) -> str:
        stack = inspect.stack()[::-1]
        stack_names = (inspect.getmodulename(stack[0].filename),
                       *(frame.function
                         for frame
                         in stack[1:-9]))
        return '::'.join(stack_names)

    def format(self, record):
        record.message = record.getMessage()
        record.stack_info = self.formatStack()
        if self.usesTime():
            record.asctime = self.formatTime(record, self.datefmt)
        s = self.formatMessage(record)
        if record.exc_info:
            # Cache the traceback text to avoid converting it multiple times
            # (it's constant anyway)
            if not record.exc_text:
                record.exc_text = self.formatException(record.exc_info)
        if record.exc_text:
            if s[-1:] != "\n":
                s = s + "\n"
            s = s + record.exc_text
        return s


def _build_default_formatter(level:int,child_name:str,colr_id:int, formatter_class=None)->Union[logging.Formatter, CallStackFormatter]:
    if formatter_class is None:
        formatter_class = logging.Formatter
    if isinstance(formatter_class,CallStackFormatter):
        stack_info = "\n\t[ %(levelname)-5s ] [ %(stack_info)s ]"
    else:
        stack_info = ""
    if level < logging.WARNING:  # logging.WARNING == 30
        fstr = LOW_PRI_FORMATTER
        header = LOW_PRI_LOGGER_HEADER_COLOR_PREFIX.format(colr_id)
    else:
        fstr = HIGH_PRI_FORMATTER
        header = HIGH_PRI_LOGGER_HEADER_COLOR_PREFIX.format(colr_id)
    return formatter_class(
        fstr.format(
            child_name=child_name,
            stack_info=stack_info,
            header=header,
            msg_style=LOGGER_MSG_COLOR.format(colr_id),
            reset=RESET),
        "%H:%M:%S")



def _ensured_configure_handler(formatter:logging.Formatter, level:int,
                               handler_delegate:Union[logging.StreamHandler,Iterable[logging.StreamHandler]],
                               handler_kwargs:dict)->Union[logging.Handler,list[logging.Handler]]:
    if handler_delegate==logging.StreamHandler:
        handler_delegate = [handler_delegate]
    handlers = []
    for delegate in handler_delegate:
        handler_kwargs.setdefault("stream", SYS_STDOUT)
        handler = delegate(**handler_kwargs)
        handler.setLevel(level)
        handler.setFormatter(formatter)
        handlers.append(handler)
    if len(handlers)==1:
        return handlers[0]
    return handlers


def _logger_setup(logger: logging.Logger,
                  header_label: str,
                  formatter: Formatter = None,
                  level: int = logging.INFO,
                  level_normalized: int = logging.INFO,
                  handler_delegate=Union[logging.StreamHandler,Iterable[logging.StreamHandler]],
                  **handler_kwargs) ->logging.Logger:
    """A convenience function for creating well named loggers with optional custom formatter.

    This function will implement an already defined formatter for you if the formatter param is
    None. For an example of a good general use logger see the example code at the bottom of this
    file.

    SPECIAL NOTE:
        Although this function's signature allows the caller to pass virtually any logging.Handler
        subclass into the handler_delegate parameter, I've only tested this functionality against
        the logging.StreamHandler class. If you wish to use it for others you will likely encounter
        bugs. But if you are up to the task it seems like it could potentially be a helpful tool for
        allowing the instantiation of a wide array of utility loggers under a single interface.

    :param header_label:
    :param logger: an initialized logger object that needs to have its formatter and optinoal handlers set.

    :param child_name:  The auxiliary logger you wish to create to handle some specific task. The
                        logger which maps to this child will set its level, handlers, and formatters
                        according to your inputs, allowing you to specify many different loggers to
                        manage data output in a form that suites you.
    :type child_name:   A string

    :param formatter:   An optional parameter that specifies the manner in which you wish to format
                        the available logging-event-_data as well as how you wish to present the
                        message _data for your log events. The default formatter will take one of
                        two styles, based the given `level`.

                        For level==logging.INFO (20) and bellow:
                            log messages will be presented in two parts.
                                * First, a header line that's formatted to be bold, and underlined,
                                  that gives the time-stamp for when the log was submitted, the
                                  child_name, the model and function and line number from which the
                                  log-message was originated
                                * Followed by an indented new-line where the log-message will be
                                  be printed. The message
                        For level==logging.INFO+1 (21) and above:
                            log messages will be presented in two parts.
                                * First, a header line that's formatted to be bold, and underlined,
                                  that gives the time-stamp for when the log was submitted, the
                                  child_name, the model and function and line number from which the
                                  log-message was originated
                                * Followed, on the same line as the header, the log-message. This
                                  distinction, as opposed to the indented new-line in lower level
                                  messaged, is done because it is often the case the when higher
                                  level messages occur, there are very many of them. Forcing each
                                  one to then be a multi-line message actually makes it much harder
                                  to visually parse.
                                * Special note: In order to aid in automated parsing of these
                                  log-messages, the header details and log message will be seperated
                                  by the following character key:
                                        `::>`

    :type formatter:    an instance of logging.Formatter

    :param handler_delegate: An optional parameter that Specifies the type of handler you want to
                             associate to the logger instance that's mapped to the
                             root_name.child_name you've passed in. The handler will be set up
                             inside of this function, this parameter simply allows you to indicate
                             the way you wish to have your output handled.
                             (E.G. to std_out, std_err, or some file output stream)
    :type handler_delegate:  This should be a delegate function of your desired handler's
                             constructor, DEFAULT=logging.StreamHandler

    :param level:   Specifies the desired logging level of the resulting logger.
                    DEFAULT=logging.DEBUG
    :type level:    An int, must be in the range of [0,0xffffffff]

    :type handler_kwargs: Any additional keywords that should be passed into the construction of the
                          handler. These are necessary for the instantiation of handlers that will
                          output to anything other than sys.std_out, and sys.std_err.

    :return:    a reference to the logger instance that's mapped to the input naming scheme of
                <root_name>.<child_name >
    :rtype: logging.Logger
    """
    logger.propagate = False
    level_normalized = level if level_normalized is None else level_normalized
    try:
        colr_id = log_id_map[level_normalized]
    except KeyError:
        # level_normalized isn't in our custom log_id_map
        if level_normalized in logging._levelToName:
            # but it has been registered with the logging library
            LEVELS_STR2INT[logging._levelToName[level_normalized]] = level_normalized
        for lvl,color_code in sorted(log_id_map.items(),key=lambda tpl:tpl[0]):
            if lvl<=level_normalized:
                colr_id = color_code
            else:
                break
        else:
            colr_id = log_id_map["NOTSET"]
        log_id_map[level_normalized] = colr_id
    if formatter is None:
        formatter = _build_default_formatter(level,header_label,colr_id,formatter)
    logger.addHandler(_ensured_configure_handler(formatter, level, handler_delegate, handler_kwargs))
    return logger


def get_logger(root_name: str,
               child_name: str = None,
               header_label: str = None,
               formatter: Union[Formatter,str] = None,
               level: Union[int, str] = None,
               logger_registration_dict: dict = None,
               handler_delegate:Union[logging.StreamHandler,Iterable[logging.StreamHandler]]=logging.StreamHandler,
               do_file_output:bool=False,
               **handler_kwargs)->logging.Logger:
    global LOGGER_COLLECTION
    if logger_registration_dict is None:
        logger_registration_dict = LOGGER_COLLECTION
    if isinstance(formatter,str):
        if "callstackformatter" == formatter.lower():
            formatter = CallStackFormatter
    level = level if level is not None else (logging.DEBUG if DEBUGGING else logging.NOTSET)
    if isinstance(level, str):
        level = LEVELS_STR2INT.get(level.upper(), logging.NOTSET)
    if not (logging.NOTSET<=level<=logging.CRITICAL):
        _logger_setup_internal_warning_logger(f"NOTICE: The level passed to the logger_setup function was not in the range of [{logging.NOTSET},{logging.CRITICAL}] and is going to be clipped to those bounds")
    level_normalized = max(level, logging.NOTSET)
    while level_normalized>logging.NOTSET:
        name = logging._levelToName.get(level_normalized,None)
        if name is not None:
            break
        level_normalized -= 1
    if child_name is not None:
        child_name = child_name if child_name else logging.getLevelName(level_normalized)
        header_label = f"{root_name}.{child_name or header_label}"
        logger = logging.getLogger(root_name).getChild(child_name)
    else:
        header_label = root_name
        logger = logging.getLogger(root_name)
    _logger_setup_kwargs = dict(logger=logger, header_label=header_label, formatter=formatter, level=level,
                                level_normalized=level_normalized, handler_delegate=handler_delegate, **handler_kwargs)
    if logger.name not in logger_registration_dict:
        logger_registration_dict[logger.name] = _logger_setup(**_logger_setup_kwargs)
    return logger_registration_dict[logger.name]


def get_logging_file_target(level:str or int, fname:str or Path, just_first:bool=True):
    def do_backup(p):
        if p.exists():
            backup = p.with_name(p.name + ".backup")
            with open(backup, "a", encoding="utf-8") as f:
                f.write(p.read_text("utf-8"))
        with open(p, "w", encoding="utf-8") as f:
            f.write(f"{level} -- START: {dt.now()}\n")
        return p
    if isinstance(level,int):
        level = LEVELS_INT2STR.get(level,logging.getLevelName(level))
    level = level.upper()
    if not fname:
        raise ValueError("Received an empty file name when deriving output file path for logging")
    for k,v in logging_target_files.items():
        if isinstance(v,str):
            path = project_root_folder.joinpath(v[3:]).parent
            path.mkdir(parents=True,exist_ok=True)
        else:
            for sk,sv in v.items():
                path = project_root_folder.joinpath(f"special/{sv[3:]}").parent
                path.mkdir(parents=True, exist_ok=True)
    if "SPECIAL" in level:
        raise NotImplementedError(f"Handling of special case logging targets has not be implemented yet.\n\ttarget fname: {fname}")
    if fname.endswith(".log"):
        fname = fname.strip(".log")
    target = logging_target_files[level].format(fname)
    ret = []
    path_gen = project_root_folder.rglob(target)
    if just_first:
        try:
            return [do_backup(next(path_gen))]
        except GeneratorExit:
            # file does not already exist, so we need to create it.
            return [do_backup(project_root_folder.joinpath(target[3:]))]
    for p in path_gen:
        ret.append(do_backup(p))
    if not ret:
        # no paths match search pattern, so we need to create an appropriate path
        ret.append(do_backup(project_root_folder.joinpath(target[3:])))
    return ret

_logger_setup_internal_warning_logger = get_logger(__name__, "logger_setup_warning", level="WARNING",logger_registration_dict={})
_logger_setup_internal_warning_logger = _logger_setup_internal_warning_logger.warning


# project_root = project_root_folder.name # gets the name of the project's root directory
project_root = proj_name # gets the name of the project's root directory
import_warnings_logger = get_logger(project_root,__name__+": import warnings",level="WARNING").warning
root_info_logger = get_logger(project_root,__name__+": INFO log",level="INFO")


# third party logging supression
if DEBUGGING:
    for loggername in "numba,azure,azure.core".split(","):
        try:
            logging.getLogger(loggername).setLevel(logging.WARNING)
        except BaseException:
            pass
This is a general purpose utility file for setting up logger output that's good for quick visual scans of a program's diagnostic data.  By: <NAME> Email: <EMAIL> # builtin imports # custom project imports ######################################################################################################################## # Determining if we are running on Windows, then set the global variable USE_COLORS accordingly. ######################################################################################################################## ######################################################################################################################## # Determining if we are running in debug mode, then set the global variable DEBUGGING accordingly. # # DEBUGGING should now be imported to the project's root __init__.py file for consistent # access across the project ######################################################################################################################## # Pycharm IDE specific implementation detail # This detects if we launched into debug mode using the Pycharm IDE # Pycharm debugger isn't running, so let's check if the code was called # from a terminal with the `-d` flag set (`-d` tells python interpreter to run in debug mode) # note that sys.flags is a namedtuple object # the following "table" of _data was taken from the official python docs for # python version 3.6.8 on May 5, 2019 # found here: https://docs.python.org/3.6/library/logging.html # see that link for the full description of the available attributes # # Attribute name	    Format # args	                You shouldn’t need to format this yourself. # asctime	            %(asctime)s # created	            %(created)f # exc_info	            You shouldn’t need to format this yourself. # filename	            %(filename)s # funcName	            %(funcName)s # levelname	            %(levelname)s # levelno	            %(levelno)s # lineno	            %(lineno)d # message	            %(message)s # module	            %(module)s # msecs	                %(msecs)d # msg	                You shouldn’t need to format this yourself. # name	                %(name)s # pathname	            %(pathname)s # process	            %(process)d # processName	        %(processName)s # relativeCreated	    %(relativeCreated)d # thread	            %(thread)d # threadName	        %(threadName)s # Level	    Numeric value # CRITICAL  50 # ERROR     40 # WARNING   30 # INFO      20 # DEBUG     10 # NOTSET    0 # charcoal # purple # green -- might be just my eyes, but green(32) and yellow(33) look almost identical # dull-cyan # white-dull # bright-purple # bright-red # logging.addLevelName(35,"EXCEPTION") # "EXCEPTION": 35, # for future cases where the code requires a clearly named custom implementation # place that implementation here and special handling can be developed for it. This custom formatter class was taken directly from:         https://stackoverflow.com/q/54747730/7412747 # Cache the traceback text to avoid converting it multiple times # (it's constant anyway) # logging.WARNING == 30 A convenience function for creating well named loggers with optional custom formatter.      This function will implement an already defined formatter for you if the formatter param is     None. For an example of a good general use logger see the example code at the bottom of this     file.      SPECIAL NOTE:         Although this function's signature allows the caller to pass virtually any logging.Handler         subclass into the handler_delegate parameter, I've only tested this functionality against         the logging.StreamHandler class. If you wish to use it for others you will likely encounter         bugs. But if you are up to the task it seems like it could potentially be a helpful tool for         allowing the instantiation of a wide array of utility loggers under a single interface.      :param header_label:     :param logger: an initialized logger object that needs to have its formatter and optinoal handlers set.      :param child_name:  The auxiliary logger you wish to create to handle some specific task. The                         logger which maps to this child will set its level, handlers, and formatters                         according to your inputs, allowing you to specify many different loggers to                         manage data output in a form that suites you.     :type child_name:   A string      :param formatter:   An optional parameter that specifies the manner in which you wish to format                         the available logging-event-_data as well as how you wish to present the                         message _data for your log events. The default formatter will take one of                         two styles, based the given `level`.                          For level==logging.INFO (20) and bellow:                             log messages will be presented in two parts.                                 * First, a header line that's formatted to be bold, and underlined,                                   that gives the time-stamp for when the log was submitted, the                                   child_name, the model and function and line number from which the                                   log-message was originated                                 * Followed by an indented new-line where the log-message will be                                   be printed. The message                         For level==logging.INFO+1 (21) and above:                             log messages will be presented in two parts.                                 * First, a header line that's formatted to be bold, and underlined,                                   that gives the time-stamp for when the log was submitted, the                                   child_name, the model and function and line number from which the                                   log-message was originated                                 * Followed, on the same line as the header, the log-message. This                                   distinction, as opposed to the indented new-line in lower level                                   messaged, is done because it is often the case the when higher                                   level messages occur, there are very many of them. Forcing each                                   one to then be a multi-line message actually makes it much harder                                   to visually parse.                                 * Special note: In order to aid in automated parsing of these                                   log-messages, the header details and log message will be seperated                                   by the following character key:                                         `::>`      :type formatter:    an instance of logging.Formatter      :param handler_delegate: An optional parameter that Specifies the type of handler you want to                              associate to the logger instance that's mapped to the                              root_name.child_name you've passed in. The handler will be set up                              inside of this function, this parameter simply allows you to indicate                              the way you wish to have your output handled.                              (E.G. to std_out, std_err, or some file output stream)     :type handler_delegate:  This should be a delegate function of your desired handler's                              constructor, DEFAULT=logging.StreamHandler      :param level:   Specifies the desired logging level of the resulting logger.                     DEFAULT=logging.DEBUG     :type level:    An int, must be in the range of [0,0xffffffff]      :type handler_kwargs: Any additional keywords that should be passed into the construction of the                           handler. These are necessary for the instantiation of handlers that will                           output to anything other than sys.std_out, and sys.std_err.      :return:    a reference to the logger instance that's mapped to the input naming scheme of                 <root_name>.<child_name >     :rtype: logging.Logger # level_normalized isn't in our custom log_id_map # but it has been registered with the logging library # file does not already exist, so we need to create it. # no paths match search pattern, so we need to create an appropriate path # project_root = project_root_folder.name # gets the name of the project's root directory # gets the name of the project's root directory # third party logging supression