Source code for BAC0.core.utils.notes

# -*- coding: utf-8 -*-
"""
Notes and logger decorator to be used on class
This will add a "notes" object to the class and will allow
logging feature at the same time.
Goal is to be able to access quickly to important informations for
the web interface.
"""
import logging
import os
import sys
import typing as t

# --- standard Python modules ---
from collections import namedtuple
from datetime import datetime
from logging import FileHandler, Logger
from os.path import expanduser, join

# --- 3rd party modules ---
try:
    import pandas as pd

    _PANDAS = True
except ImportError:
    _PANDAS = False


[docs]class LogList: LOGGERS: t.List[Logger] = []
[docs]def convert_level(level): if not level: return None _valid_levels = [ logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL, ] if level in _valid_levels: return level if level.lower() == "info": return logging.INFO elif level.lower() == "debug": return logging.DEBUG elif level.lower() == "warning": return logging.WARNING elif level.lower() == "error": return logging.ERROR elif level.lower() == "critical": return logging.CRITICAL raise ValueError( "Wrong log level use one of the following : {}".format(_valid_levels) )
[docs]def update_log_level( level=None, *, log_file=None, stderr=None, stdout=None, log_this=True ): """ Typical usage :: # Silence (use CRITICAL so not much messages will be sent) BAC0.log_level('silence') # Verbose BAC0.log_level('info') # Default, Info on console....but Warning in file BAC0.log_level(file='warning', stdout='info', stderr='critical') # Debug in file and console... this is a bad idea as the console will be filled BAC0.log_level(file='debug', stdout='debug', stderr='critical') # Preferably, debug in the file, keep console limited to info BAC0.log_level('debug') # OR BAC0.log_level(file='debug', stdout='info', stderr='critical') Giving only one parameter will set file and console to the same level. I tend to keep stderr CRITICAL """ update_log_file_lvl = False update_stderr_lvl = False update_stdout_lvl = False if level: logging.getLogger("BAC0_Root.BAC0.scripts.Base.Base").disabled = False if level.lower() == "silence": log_file_lvl = logging.CRITICAL stderr_lvl = logging.CRITICAL stdout_lvl = logging.CRITICAL update_log_file_lvl = True update_stderr_lvl = True update_stdout_lvl = True logging.getLogger("BAC0_Root.BAC0.scripts.Base.Base").disabled = True elif level.lower() == "default": log_file_lvl = logging.WARNING stderr_lvl = logging.CRITICAL stdout_lvl = logging.INFO update_log_file_lvl = True update_stderr_lvl = True update_stdout_lvl = True elif level.lower() == "debug": log_file_lvl = logging.DEBUG stdout_lvl = logging.INFO update_log_file_lvl = True update_stdout_lvl = True else: level = convert_level(level) log_file_lvl = level stdout_lvl = level update_log_file_lvl = True update_stdout_lvl = True else: if log_file: log_file_lvl = convert_level(log_file) update_log_file_lvl = True if stderr: stderr_lvl = convert_level(stderr) update_stderr_lvl = True if stdout: stdout_lvl = convert_level(stdout) update_stdout_lvl = True # Choose Base as logger for this task if log_this: BAC0_logger = logging.getLogger("BAC0_Root.BAC0.scripts.Base.Base") for each in LogList.LOGGERS: for handler in each.handlers: if update_log_file_lvl and handler.get_name() == "file_handler": handler.setLevel(log_file_lvl) if log_this: BAC0_logger.warning( "Changed log level of file to {}".format( logging.getLevelName(log_file_lvl) ) ) elif update_stdout_lvl and handler.get_name() == "stdout": handler.setLevel(stdout_lvl) if log_this: BAC0_logger.warning( "Changed log level of console stdout to {}".format( logging.getLevelName(stdout_lvl) ) ) elif update_stderr_lvl and handler.get_name() == "stderr": handler.setLevel(stderr_lvl) if log_this: BAC0_logger.warning( "Changed log level of console stderr to {}".format( logging.getLevelName(stderr_lvl) ) )
[docs]def note_and_log(cls): """ This will be used as a decorator on class to activate logging and store messages in the variable cls._notes This will allow quick access to events in the web app. A note can be added to cls._notes without logging if passing the argument log=false to function note() Something can be logged without addind a note using function log() """ if hasattr(cls, "DEBUG_LEVEL"): if cls.DEBUG_LEVEL == "debug": file_level = logging.DEBUG console_level = logging.DEBUG elif cls.DEBUG_LEVEL == "info": file_level = logging.INFO console_level = logging.INFO else: file_level = logging.WARNING console_level = logging.INFO # Notes object cls._notes = namedtuple("_notes", ["timestamp", "notes"]) cls._notes.timestamp = [] cls._notes.notes = [] # Defining log object cls.logname = "{} | {}".format(cls.__module__, cls.__name__) cls._log = logging.getLogger("BAC0_Root.{}.{}".format(cls.__module__, cls.__name__)) # Set level to debug so filter is done by handler cls._log.setLevel(logging.DEBUG) # Console Handler ch = logging.StreamHandler(sys.stderr) ch.set_name("stderr") ch.setLevel(logging.CRITICAL) ch2 = logging.StreamHandler(sys.stdout) ch2.set_name("stdout") ch2.setLevel(console_level) formatter = logging.Formatter("{asctime} - {levelname:<8}| {message}", style="{") # File Handler _PERMISSION_TO_WRITE = True logUserPath = expanduser("~") logSaveFilePath = join(logUserPath, ".BAC0") logFile = join(logSaveFilePath, "BAC0.log") try: if not os.path.exists(logSaveFilePath): os.makedirs(logSaveFilePath) fh = FileHandler(logFile) fh.set_name("file_handler") fh.setLevel(file_level) fh.setFormatter(formatter) except OSError: _PERMISSION_TO_WRITE = False ch.setFormatter(formatter) ch2.setFormatter(formatter) # Add handlers the first time only... if not len(cls._log.handlers): if _PERMISSION_TO_WRITE: cls._log.addHandler(fh) cls._log.addHandler(ch) cls._log.addHandler(ch2) LogList.LOGGERS.append(cls._log) def log_title(self, title, args=None, width=35): cls._log.debug("") cls._log.debug("#" * width) cls._log.debug("# {}".format(title)) cls._log.debug("#" * width) if args: cls._log.debug("{!r}".format(args)) cls._log.debug("#" * 35) def log_subtitle(self, subtitle, args=None, width=35): cls._log.debug("") cls._log.debug("=" * width) cls._log.debug("{}".format(subtitle)) cls._log.debug("=" * width) if args: cls._log.debug("{!r}".format(args)) cls._log.debug("=" * width) def log(self, note, *, level=logging.DEBUG): """ Add a log entry...no note """ if not note: raise ValueError("Provide something to log") note = "{} | {}".format(cls.logname, note) cls._log.log(level, note) def note(self, note, *, level=logging.INFO, log=True): """ Add note to the object. By default, the note will also be logged :param note: (str) The note itself :param level: (logging.level) :param log: (boolean) Enable or disable logging of note """ if not note: raise ValueError("Provide something to log") note = "{} | {}".format(cls.logname, note) cls._notes.timestamp.append(datetime.now().astimezone()) cls._notes.notes.append(note) if log: cls.log(level, note) @property def notes(self): """ Retrieve notes list as a Pandas Series """ if not _PANDAS: return dict(zip(self._notes.timestamp, self._notes.notes)) return pd.Series(self._notes.notes, index=self._notes.timestamp) def clear_notes(self): """ Clear notes object """ cls._notes.timestamp = [] cls._notes.notes = [] # Add the functions to the decorated class cls.clear_notes = clear_notes cls.note = note cls.notes = notes cls.log = log cls.log_title = log_title cls.log_subtitle = log_subtitle return cls