Source code for oasislmf.pytools.utils

"""
This file contains general-purpose utilities.
"""
import logging
import numpy as np
import os
import uuid


[docs] def logging_set_handlers(logger_name, handler, log_level): logger = logging.getLogger(logger_name) # set all handlers to ERROR for handler in logger.handlers: handler.setLevel(logging.ERROR) # set children oasislmf loggers to 'log_level' if 'oasislmf.' in logger_name: logger.addHandler(handler) logger.setLevel(log_level) logger.propagate = False else: logger.setLevel(logging.ERROR)
[docs] def logging_reset_handlers(logger_name): logger = logging.getLogger(logger_name) # revert all handlers to NOTSET for handler in logger.handlers: handler.setLevel(logging.NOTSET) logger.propagate = True # Remove added handlers if 'oasislmf.' in logger_name: logger.handlers.clear() else: logger.setLevel(logging.NOTSET)
[docs] def redirect_logging(exec_name, log_dir='./log', log_level=logging.WARNING): """ Decorator that redirects logging output to a file. Apply to the main run function of a python exec from the pytools directory. Only errors will be send to STDERR, all other logging is stored in a file named: "<log_dir>/<exec_name>_<PID>.log" Each log file is timestamped with start / finish times ❯ cat log/fmpy_112820.log 2023-03-01 13:48:31,286 - oasislmf - INFO - starting process 2023-03-01 13:48:36,476 - oasislmf - INFO - finishing process Args: exec_name (str): The name of the script or function being executed. This will be used as part of the log file name. log_dir (str, optional): The path to the directory where log files will be stored. Defaults to './log'. log_level (int or str, optional): The logging level to use. Can be an integer or a string. Defaults to logging.INFO. Returns: function: The decorated function. Example: @redirect_logging(exec_name='my_script', log_dir='./logs', log_level=logging.DEBUG) def my_run_function(): # code here """ def inner(func): def wrapper(*args, **kwargs): if not os.path.isdir(log_dir): os.makedirs(log_dir) logging_config = logging.root.manager.loggerDict.keys() logging.captureWarnings(True) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') log_file = f'{exec_name}_{os.getpid()}_{uuid.uuid4()}.log' childFileHandler = logging.FileHandler(os.path.join(log_dir, log_file)) childFileHandler.setLevel(log_level) childFileHandler.setFormatter(formatter) rootFileHandler = logging.FileHandler(os.path.join(log_dir, log_file)) rootFileHandler.setLevel(logging.INFO) rootFileHandler.setFormatter(formatter) # Set all logger handlers to level ERROR for lg_name in logging_config: logging_set_handlers(lg_name, childFileHandler, log_level) # Set root oasislmf logger to INFO logger = logging.getLogger('oasislmf') logger.setLevel(logging.INFO) logger.addHandler(rootFileHandler) # Set warning log handler warn_logger = logging.getLogger('py.warnings') warn_logger.addHandler(rootFileHandler) # # Debug: print logging tree # import ipdb; ipdb.set_trace() # import logging_tree; logging_tree.printout() try: logger.info(kwargs) logger.info('starting process') # Run the wrapped function retval = func(*args, **kwargs) logger.info('finishing process') return retval except Exception as err: logger.exception(err) raise err finally: for lg_name in logging_config: logging_reset_handlers(lg_name) logger.removeHandler(rootFileHandler) logging.shutdown() logging.captureWarnings(False) return wrapper return inner
[docs] def assert_allclose(x, y, rtol=1e-10, atol=1e-8, x_name="x", y_name="y"): """ Drop in replacement for `numpy.testing.assert_allclose` that also shows the nonmatching elements in a nice human-readable format. Args: x (np.array or scalar): first input to compare. y (np.array or scalar): second input to compare. rtol (float, optional): relative tolreance. Defaults to 1e-10. atol (float, optional): absolute tolerance. Defaults to 1e-8. x_name (str, optional): header to print for x if x and y do not match. Defaults to "x". y_name (str, optional): header to print for y if x and y do not match. Defaults to "y". Raises: AssertionError: if x and y shapes do not match. AssertionError: if x and y data do not match. """ if np.isscalar(x) and np.isscalar(y) == 1: return np.testing.assert_allclose(x, y, rtol=rtol, atol=atol) if x.shape != y.shape: raise AssertionError("Shape mismatch: %s vs %s" % (str(x.shape), str(y.shape))) d = ~np.isclose(x, y, rtol, atol) if np.any(d): miss = np.where(d)[0] msg = f"Mismatch of {len(miss):d} elements ({len(miss) / x.size * 100:g} %) at the level of rtol={rtol:g}, atol={atol:g},\n" \ f"{repr(miss)}\n" \ f"x: {x_name}\n{str(x[d])}\n\n" \ f"y: {y_name}\n{str(y[d])}"\ raise AssertionError(msg)