# -*- coding: utf-8 -*- # ***************************************************************************** # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Module authors: # Enrico Faulhaber # # ***************************************************************************** """Define helpers""" import importlib import linecache import socket import sys import threading import traceback from configparser import ConfigParser from os import environ, path class GeneralConfig: """generalConfig holds server configuration items generalConfig.init is to be called before starting the server. Accessing generalConfig. raises an error, when generalConfig.init is not yet called, except when a default for is set. For tests and for imports from client code, a module may access generalConfig without calling generalConfig.init before. For this, it should call generalConfig.set_default on import to define defaults for the needed keys. """ def __init__(self): self._config = None self.defaults = {} #: default values. may be set before or after :meth:`init` def init(self, configfile=None): """init default server configuration :param configfile: if present, keys and values from the [FRAPPY] section are read if configfile is not given, it tries to guess the location of the configfile or determine 'piddir', 'logdir', 'confdir' and 'basedir' from the environment. """ cfg = {} mandatory = 'piddir', 'logdir', 'confdir' repodir = path.abspath(path.join(path.dirname(__file__), '..', '..')) # create default paths if path.splitext(sys.executable)[1] == ".exe" and not path.basename(sys.executable).startswith('python'): # special MS windows environment cfg.update(piddir='./', logdir='./log', confdir='./') elif path.exists(path.join(repodir, '.git')): # running from git repo cfg['confdir'] = path.join(repodir, 'cfg') # take logdir and piddir from /cfg/generalConfig.cfg else: # running on installed system (typically with systemd) cfg.update(piddir='/var/run/frappy', logdir='/var/log', confdir='/etc/frappy') if configfile is None: configfile = environ.get('FRAPPY_CONFIG_FILE', path.join(cfg['confdir'], 'generalConfig.cfg')) configfile = path.expanduser(configfile) if not path.exists(configfile): raise FileNotFoundError(configfile) if configfile and path.exists(configfile): parser = ConfigParser() parser.optionxform = str parser.read([configfile]) # mandatory in a general config file: cfg['logdir'] = cfg['piddir'] = None cfg['confdir'] = path.dirname(configfile) # only the FRAPPY section is relevant, other sections might be used by others for key, value in parser['FRAPPY'].items(): if value.startswith('./'): cfg[key] = path.abspath(path.join(repodir, value)) else: # expand ~ to username, also in path lists separated with ':' cfg[key] = ':'.join(path.expanduser(v) for v in value.split(':')) else: for key in mandatory: cfg[key] = environ.get('FRAPPY_%s' % key.upper(), cfg[key]) missing_keys = [key for key in mandatory if cfg[key] is None] if missing_keys: if path.exists(configfile): raise KeyError('missing value for %s in %s' % (' and '.join(missing_keys), configfile)) raise FileNotFoundError(configfile) # this is not customizable cfg['basedir'] = repodir self._config = cfg def __getitem__(self, key): """access for keys known to exist :param key: the key (raises an error when key is not available) :return: the value """ try: return self._config[key] except KeyError: return self.defaults[key] except TypeError: if key in self.defaults: # accept retrieving defaults before init # e.g. 'lazy_number_validation' in secop.datatypes return self.defaults[key] raise TypeError('generalConfig.init() has to be called first') from None def get(self, key, default=None): """access for keys not known to exist""" try: return self.__getitem__(key) except KeyError: return default def getint(self, key, default=None): """access and convert to int""" try: return int(self.__getitem__(key)) except KeyError: return default def __getattr__(self, key): """goodie: use generalConfig. instead of generalConfig.get('')""" return self.get(key) @property def initialized(self): return bool(self._config) def set_default(self, key, value): """set a default value, in case not set already""" if key not in self.defaults: self.defaults[key] = value def testinit(self, **kwds): """for test purposes""" self._config = kwds generalConfig = GeneralConfig() class lazy_property: """A property that calculates its value only once.""" def __init__(self, func): self._func = func self.__name__ = func.__name__ self.__doc__ = func.__doc__ def __get__(self, obj, obj_class): if obj is None: return self obj.__dict__[self.__name__] = self._func(obj) return obj.__dict__[self.__name__] class attrdict(dict): """a normal dict, providing access also via attributes""" def __getattr__(self, key): return self[key] def __setattr__(self, key, value): self[key] = value def clamp(_min, value, _max): """return the median of 3 values, i.e. value if min <= value <= max, else min or max depending on which side value lies outside the [min..max] interval. This works even when min > max! """ # return median, i.e. clamp the the value between min and max return sorted([_min, value, _max])[1] def get_class(spec): """loads a class given by string in dotted notation (as python would do)""" modname, classname = spec.rsplit('.', 1) if modname.startswith('secop'): module = importlib.import_module(modname) else: # rarely needed by now.... module = importlib.import_module('secop.' + modname) try: return getattr(module, classname) except AttributeError: raise AttributeError('no such class') from None def mkthread(func, *args, **kwds): t = threading.Thread( name='%s:%s' % (func.__module__, func.__name__), target=func, args=args, kwargs=kwds) t.daemon = True t.start() return t def formatExtendedFrame(frame): ret = [] for key, value in frame.f_locals.items(): try: valstr = repr(value)[:256] except Exception: valstr = '' ret.append(' %-20s = %s\n' % (key, valstr)) ret.append('\n') return ret def formatExtendedTraceback(exc_info=None): if exc_info is None: etype, value, tb = sys.exc_info() else: etype, value, tb = exc_info ret = ['Traceback (most recent call last):\n'] while tb is not None: frame = tb.tb_frame filename = frame.f_code.co_filename item = ' File "%s", line %d, in %s\n' % (filename, tb.tb_lineno, frame.f_code.co_name) linecache.checkcache(filename) line = linecache.getline(filename, tb.tb_lineno, frame.f_globals) if line: item = item + ' %s\n' % line.strip() ret.append(item) if filename not in ('