From 992c5b4c691660b26c97712beb8f29237f14bd15 Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Thu, 16 Mar 2017 15:40:42 +0100 Subject: [PATCH] Adding classes for configured applications --- mk-home | 43 ++++++++ pp_lib/cfg_app.py | 270 +++++++++++++++++++++++++++++++++++++++++++++ pp_lib/rec_dict.py | 72 ++++++++++++ 3 files changed, 385 insertions(+) create mode 100755 mk-home create mode 100644 pp_lib/cfg_app.py create mode 100644 pp_lib/rec_dict.py diff --git a/mk-home b/mk-home new file mode 100755 index 0000000..370973f --- /dev/null +++ b/mk-home @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +# Standard modules +import sys +import os +import logging +import locale + +# own modules: +cur_dir = os.getcwd() +base_dir = cur_dir + +if sys.argv[0] != '' and sys.argv[0] != '-c': + cur_dir = os.path.dirname(sys.argv[0]) +if os.path.exists(os.path.join(cur_dir, 'pp_lib')): + sys.path.insert(0, os.path.abspath(cur_dir)) + +from pp_lib.common import pp + +from pp_lib.cfg_app import PpConfigApplication + +log = logging.getLogger(__name__) + +__author__ = 'Frank Brehm ' +__copyright__ = '(C) 2017 by Frank Brehm, Pixelpark GmbH, Berlin' + +appname = os.path.basename(sys.argv[0]) + +locale.setlocale(locale.LC_ALL, '') + +app = PpConfigApplication(appname=appname, cfg_dir='pixelpark') +#app.initialized = True + +if app.verbose > 2: + print("{c}-Object:\n{a}".format(c=app.__class__.__name__, a=app)) + +#app() + +sys.exit(0) + +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 + +# vim: ts=4 diff --git a/pp_lib/cfg_app.py b/pp_lib/cfg_app.py new file mode 100644 index 0000000..658ea73 --- /dev/null +++ b/pp_lib/cfg_app.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@author: Frank Brehm +@contact: frank.brehm@pixelpark.com +@copyright: © 2017 by Frank Brehm, Berlin +@summary: The module for the application object. +""" +from __future__ import absolute_import + +# Standard modules +import sys +import os +import logging +import re +import traceback +import textwrap +import datetime + +# Third party modules +import six + +from six import StringIO +from six.moves import configparser + +from configparser import Error as ConfigParseError + +import argparse + +# Own modules +from .global_version import __version__ as __global_version__ + +from .errors import FunctionNotImplementedError, PpAppError + +from .common import pp, terminal_can_colors, to_bytes + +from .rec_dict import RecursiveDictionary + +from .app import PpApplication + +__version__ = '0.2.1' +LOG = logging.getLogger(__name__) + + +# ============================================================================= +class PpCfgAppError(PpAppError): + """Base error class for all exceptions happened during + execution this configured application""" + + pass + + +# ============================================================================= +class PpConfigApplication(PpApplication): + """ + Class for configured application objects. + """ + + # ------------------------------------------------------------------------- + def __init__( + self, appname=None, verbose=0, version=__version__, base_dir=None, + initialized=None, usage=None, description=None, + argparse_epilog=None, argparse_prefix_chars='-', env_prefix=None, + cfg_dir=None, cfg_stem=None, cfg_encoding='utf-8', need_config_file=False): + + self._cfg_encoding = cfg_encoding + self._need_config_file = bool(need_config_file) + + self.cfg = RecursiveDictionary() + + self._cfg_dir = None + self._cfg_stem = None + self.cfg_files = [] + + super(PpConfigApplication, self).__init__( + appname=appname, verbose=verbose, version=version, base_dir=base_dir, + initialized=False, usage=usage, description=description, + argparse_epilog=argparse_epilog, argparse_prefix_chars=argparse_prefix_chars, + env_prefix=env_prefix, + ) + + if cfg_dir is None: + self._cfg_dir = self.appname + else: + d = str(cfg_dir).strip() + if d == '': + self._cfg_dir = None + else: + self._cfg_dir = d + + if cfg_stem: + s = str(cfg_stem).strip() + if not s: + msg = "Invalid configuration stem {!r} given.".format(cfg_stem) + raise PbCfgAppError(msg) + self._cfg_stem = s + else: + self._cfg_stem = self.appname + + self.init_cfgfiles() + + enc = getattr(self.args, 'cfg_encoding', None) + if enc: + enc = enc.lower() + if enc != self.cfg_encoding: + self._cfg_encoding = enc + + self.post_init() + self._read_config() + + # ----------------------------------------------------------- + @property + def need_config_file(self): + """ + hide command line parameter --default-config and + don't execute generation of default config + """ + return getattr(self, '_need_config_file', False) + + # ----------------------------------------------------------- + @property + def cfg_encoding(self): + """The encoding character set of the configuration files.""" + return self._cfg_encoding + + # ----------------------------------------------------------- + @property + def cfg_dir(self): + """The directory containing the configuration files.""" + return self._cfg_dir + + # ----------------------------------------------------------- + @property + def cfg_stem(self): + """The basename of the configuration file without any file extension.""" + return self._cfg_stem + + # ------------------------------------------------------------------------- + def as_dict(self, short=True): + """ + Transforms the elements of the object into a dict + + @param short: don't include local properties in resulting dict. + @type short: bool + + @return: structure as dict + @rtype: dict + """ + + res = super(PpConfigApplication, self).as_dict(short=short) + res['need_config_file'] = self.need_config_file + res['cfg_encoding'] = self.cfg_encoding + res['cfg_dir'] = self.cfg_dir + res['cfg_stem'] = self.cfg_stem + + return res + + # ------------------------------------------------------------------------- + def init_arg_parser(self): + """ + Method to initiate the argument parser. + + This method should be explicitely called by all init_arg_parser() + methods in descendant classes. + """ + self.arg_parser.add_argument( + "-C", "--cfgfile", "--cfg-file", "--config", + metavar="FILE", + dest="cfg_file", + help="Configuration file to use additional to the standard configuration files.", + ) + + self.arg_parser.add_argument( + "--cfg-encoding", + metavar="ENCODING", + dest="cfg_encoding", + default=self.cfg_encoding, + help=("The encoding character set of the configuration files " + "(default: %(default)r)."), + ) + + # ------------------------------------------------------------------------- + def init_cfgfiles(self): + """Method to generate the self.cfg_files list.""" + + self.cfg_files = [] + cfg_basename = '{}.ini'.format(self.cfg_stem) + + # add /etc/app/app.ini or $VIRTUAL_ENV/etc/app/app.ini + etc_dir = os.sep + 'etc' + if 'VIRTUAL_ENV' in os.environ: + etc_dir = os.path.join(os.environ['VIRTUAL_ENV'], 'etc') + syscfg_fn = None + if self.cfg_dir: + syscfg_fn = os.path.join(etc_dir, self.cfg_dir, cfg_basename) + else: + syscfg_fn = os.path.join(etc_dir, cfg_basename) + self.cfg_files.append(syscfg_fn) + + # add /etc/app.ini + mod_dir = os.path.dirname(__file__) + work_dir = os.path.abspath(os.path.join(mod_dir, '..')) + work_etc_dir = os.path.join(work_dir, 'etc') + if self.verbose > 1: + LOG.debug("Searching for {!r} ...".format(work_etc_dir)) + self.cfg_files.append(os.path.join(work_etc_dir, cfg_basename)) + + # add $HOME/.config/app.ini + usercfg_fn = None + user_cfg_dir = os.path.expanduser('~/.config') + if user_cfg_dir: + if self.cfg_dir: + user_cfg_dir = os.path.join(user_cfg_dir, self.cfg_dir) + if self.verbose > 1: + LOG.debug("user_cfg_dir: {!r}".format(user_cfg_dir)) + usercfg_fn = os.path.join(user_cfg_dir, cfg_basename) + self.cfg_files.append(usercfg_fn) + + # add a configfile given on command line with --cfg-file + cmdline_cfg = getattr(self.args, 'cfg_file', None) + if cmdline_cfg: + self.cfg_files.append(cmdline_cfg) + + # ------------------------------------------------------------------------- + def _read_config(self): + + if self.verbose > 0: + LOG.debug("Reading config files with character set {!r} ...".format( + self.cfg_encoding)) + + open_opts = {} + if six.PY3 and self.cfg_encoding: + open_opts['encoding'] = self.cfg_encoding + open_opts['errors'] = 'surrogateescape' + + found_cfg_files = False + for cfg_file in self.cfg_files: + if self.verbose > 2: + LOG.debug("Searching for {!r} ...".format(cfg_file)) + if not os.path.isfile(cfg_file): + if self.verbose > 3: + LOG.debug("Config file {!r} not found.".format(cfg_file)) + continue + if self.verbose > 1: + LOG.debug("Reading {!r} ...".format(cfg_file)) + found_cfg_files = True + + config = configparser.ConfigParser() + try: + with open(cfg_file, 'r', **open_opts) as fh: + stream = StringIO("[default]\n" + fh.read()) + if six.PY2: + config.readfp(stram) + else: + config.read_file(stram) + except ConfigParseError as e: + msg = "Wrong configuration in {!r} found: ".format(cfg_file) + msg += str(e) + self.handle_error(msg, "Configuration error") + continue + +# ============================================================================= + +if __name__ == "__main__": + + pass + +# ============================================================================= + +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list diff --git a/pp_lib/rec_dict.py b/pp_lib/rec_dict.py new file mode 100644 index 0000000..4060a89 --- /dev/null +++ b/pp_lib/rec_dict.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@author: Frank Brehm +@summary: The module provides an object class with a dict, which can + be updated in a recursive way. + It is originated by Jannis Andrija Schnitzer:: + https://gist.github.com/114831 +""" + +# Standard modules +# import sys +# import os +import logging + +__author__ = 'jannis@itisme.org (Jannis Andrija Schnitzer)' +__copyright__ = '(c) 2009 Jannis Andrija Schnitzer' +__contact__ = 'jannis@itisme.org' +__version__ = '0.2.1' +__license__ = 'GPL3' + +log = logging.getLogger(__name__) + + +# ============================================================================= +class RecursiveDictionary(dict): + """RecursiveDictionary provides the methods rec_update and iter_rec_update + that can be used to update member dictionaries rather than overwriting + them.""" + + # ------------------------------------------------------------------------- + def rec_update(self, other, **third): + """Recursively update the dictionary with the contents of other and + third like dict.update() does - but don't overwrite sub-dictionaries. + Example: + >>> d = RecursiveDictionary({'foo': {'bar': 42}}) + >>> d.rec_update({'foo': {'baz': 36}}) + >>> d + {'foo': {'baz': 36, 'bar': 42}} + """ + + try: + iterator = iter(other.items()) + except AttributeError: + iterator = other + + self.iter_rec_update(iterator) + self.iter_rec_update(iter(third.items())) + + # ------------------------------------------------------------------------- + def iter_rec_update(self, iterator): + for (key, value) in iterator: + if key in self and \ + isinstance(self[key], dict) and isinstance(value, dict): + self[key] = RecursiveDictionary(self[key]) + self[key].rec_update(value) + else: + self[key] = value + + # ------------------------------------------------------------------------- + def __repr__(self): + return super(self.__class__, self).__repr__() + +# ============================================================================= + +if __name__ == "__main__": + + pass + +# ============================================================================= + +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 -- 2.39.5