diff options
author | Marcello Stanisci <stanisci.m@gmail.com> | 2019-03-20 22:53:14 +0100 |
---|---|---|
committer | Marcello Stanisci <stanisci.m@gmail.com> | 2019-03-20 22:53:14 +0100 |
commit | 49c3ca55986228049dd5f23cca55bccc86b7af15 (patch) | |
tree | b1c04801ed52dc82a4e53155eb043ce2e5ae62e8 /talersurvey | |
parent | d35b2decd9d488092da77a77aa08434ed50d7f11 (diff) | |
download | survey-49c3ca55986228049dd5f23cca55bccc86b7af15.tar.gz survey-49c3ca55986228049dd5f23cca55bccc86b7af15.tar.bz2 survey-49c3ca55986228049dd5f23cca55bccc86b7af15.zip |
Test and fix #5643; also updating the config module.
Diffstat (limited to 'talersurvey')
-rw-r--r-- | talersurvey/survey/survey.py | 5 | ||||
-rw-r--r-- | talersurvey/survey/templates/survey_stats.html | 2 | ||||
-rw-r--r-- | talersurvey/talerconfig.py | 391 |
3 files changed, 308 insertions, 90 deletions
diff --git a/talersurvey/survey/survey.py b/talersurvey/survey/survey.py index 65e5980..79d0f16 100644 --- a/talersurvey/survey/survey.py +++ b/talersurvey/survey/survey.py @@ -70,9 +70,10 @@ def utility_processor(): parsed_time = re.search(r"/Date\(([0-9]+)\)/", talerdate) if not parsed_time: return "malformed date given" - timestamp = datetime.datetime.fromtimestamp(parsed_time.group(1)) + parsed_time = int(parsed_time.group(1)) + timestamp = datetime.datetime.fromtimestamp(parsed_time) # returns the YYYY-MM-DD date format. - return timestamp.isoformat() + return timestamp.strftime("%Y-%b-%d") return dict(env=env, prettydate=prettydate) diff --git a/talersurvey/survey/templates/survey_stats.html b/talersurvey/survey/templates/survey_stats.html index 62c77dd..41cb437 100644 --- a/talersurvey/survey/templates/survey_stats.html +++ b/talersurvey/survey/templates/survey_stats.html @@ -6,7 +6,7 @@ merchant. Usually this should not be visible to users.</p> <ul> <li>Reserve pub: {{ stats['reserve_pub'] }}</li> - <li>Reserve expiration: {{ stats['reserve_expiration'] }}</li> + <li>Reserve expiration: {{ prettydate(stats['reserve_expiration']) }}</li> <li>Amount available {{ stats['amount_available'] }}</li> <li>Amount picked up {{ stats['amount_picked_up'] }}</li> <li>Amount authorized {{ stats['amount_authorized'] }}</li> diff --git a/talersurvey/talerconfig.py b/talersurvey/talerconfig.py index a7ca065..69d06a8 100644 --- a/talersurvey/talerconfig.py +++ b/talersurvey/talerconfig.py @@ -1,22 +1,21 @@ -# This file is part of TALER -# (C) 2016 INRIA +## +# This file is part of TALER +# (C) 2016 INRIA # -# TALER is free software; you can redistribute it and/or modify it under the -# terms of the GNU Affero General Public License as published by the Free Software -# Foundation; either version 3, or (at your option) any later version. +# TALER is free software; you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free Software +# Foundation; either version 3, or (at your option) any later version. # -# TALER 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. +# TALER 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 -# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +# You should have received a copy of the GNU General Public License along with +# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> # -# @author Florian Dold - -""" -Parse GNUnet-style configurations in pure Python -""" +# @author Florian Dold +# @author Marcello Stanisci +# @brief Parse GNUnet-style configurations in pure Python import logging import collections @@ -24,12 +23,14 @@ import os import weakref import sys import re +from typing import Callable, Any LOGGER = logging.getLogger(__name__) __all__ = ["TalerConfig"] TALER_DATADIR = None + try: # not clear if this is a good idea ... from talerpaths import TALER_DATADIR as t @@ -37,21 +38,34 @@ try: except ImportError: pass +## +# Exception class for a any configuration error. class ConfigurationError(Exception): pass +## +# Exception class for malformed strings having with parameter +# expansion. class ExpansionSyntaxError(Exception): pass - -def expand(var, getter): - """ - Do shell-style parameter expansion. - Supported syntax: - - ${X} - - ${X:-Y} - - $X - """ +## +# Do shell-style parameter expansion. +# Supported syntax: +# - ${X} +# - ${X:-Y} +# - $X +# +# @param var entire config value that might contain a parameter +# to expand. +# @param getter function that is in charge of returning _some_ +# value to be used in place of the parameter to expand. +# Typically, the replacement is searched first under the +# PATHS section of the current configuration, or (if not +# found) in the environment. +# +# @return the expanded config value. +def expand(var: str, getter: Callable[[str], str]) -> str: pos = 0 result = "" while pos != -1: @@ -88,38 +102,25 @@ def expand(var, getter): result = result + replace pos = end - return result + var[pos:] - -class OptionDict(collections.defaultdict): - def __init__(self, config, section_name): - self.config = weakref.ref(config) - self.section_name = section_name - super().__init__() - def __missing__(self, key): - entry = Entry(self.config(), self.section_name, key) - self[key] = entry - return entry - def __getitem__(self, chunk): - return super().__getitem__(chunk.lower()) - def __setitem__(self, chunk, value): - super().__setitem__(chunk.lower(), value) - - -class SectionDict(collections.defaultdict): - def __missing__(self, key): - value = OptionDict(self, key) - self[key] = value - return value - def __getitem__(self, chunk): - return super().__getitem__(chunk.lower()) - def __setitem__(self, chunk, value): - super().__setitem__(chunk.lower(), value) - - +## +# A configuration entry. class Entry: - def __init__(self, config, section, option, **kwargs): + + ## + # Init constructor. + # + # @param self the object itself. + # @param config reference to a configuration object - FIXME + # define "configuration object". + # @param section name of the config section where this entry + # got defined. + # @param option name of the config option associated with this + # entry. + # @param kwargs keyword arguments that hold the value / filename + # / line number of this current option. + def __init__(self, config, section: str, option: str, **kwargs) -> None: self.value = kwargs.get("value") self.filename = kwargs.get("filename") self.lineno = kwargs.get("lineno") @@ -127,14 +128,36 @@ class Entry: self.option = option self.config = weakref.ref(config) - def __repr__(self): + ## + # XML representation of this entry. + # + # @param self the object itself. + # @return XML string holding all the relevant information + # for this entry. + def __repr__(self) -> str: return "<Entry section=%s, option=%s, value=%s>" \ % (self.section, self.option, repr(self.value),) - def __str__(self): + ## + # Return the value for this entry, as is. + # + # @param self the object itself. + # @return the config value. + def __str__(self) -> Any: return self.value - def value_string(self, default=None, required=False, warn=False): + ## + # Return entry value, accepting defaults. + # + # @param self the object itself + # @param default default value to return if none was found. + # @param required indicate whether the value was required or not. + # If the value was required, but was not found, an exception + # is found. + # @param warn if True, outputs a warning message if the value was + # not found -- regardless of it being required or not. + # @return the value, or the given @a default, if not found. + def value_string(self, default=None, required=False, warn=False) -> str: if required and self.value is None: raise ConfigurationError("Missing required option '%s' in section '%s'" \ % (self.option.upper(), self.section.upper())) @@ -149,7 +172,16 @@ class Entry: return default return self.value - def value_int(self, default=None, required=False, warn=False): + ## + # Return entry value as a _int_. Raise exception if the + # value is not convertible to a integer. + # + # @param self the object itself + # @param default currently ignored. + # @param required currently ignored. + # @param warn currently ignored. + # @return the value, or the given @a default, if not found. + def value_int(self, default=None, required=False, warn=False) -> int: value = self.value_string(default, warn, required) if value is None: return None @@ -158,8 +190,13 @@ class Entry: except ValueError: raise ConfigurationError("Expected number for option '%s' in section '%s'" \ % (self.option.upper(), self.section.upper())) - - def _getsubst(self, key): + ## + # Fetch value to substitute to expansion variables. + # + # @param self the object itself. + # @param key the value's name to lookup. + # @return the value, if found, None otherwise. + def _getsubst(self, key: str) -> Any: value = self.config()["paths"][key].value if value is not None: return value @@ -168,31 +205,136 @@ class Entry: return value return None - def value_filename(self, default=None, required=False, warn=False): + ## + # Fetch the config value that should be a filename, + # taking care of invoking the variable-expansion logic first. + # + # @param self the object itself. + # @param default currently ignored. + # @param required currently ignored. + # @param warn currently ignored. + # @return the (expanded) filename. + def value_filename(self, default=None, required=False, warn=False) -> str: value = self.value_string(default, required, warn) if value is None: return None return expand(value, self._getsubst) - def location(self): + ## + # Give the filename and line number of this config entry. + # + # @param self this object. + # @return <filename>:<linenumber>, or "<unknown>" if one + # is not known. + def location(self) -> str: if self.filename is None or self.lineno is None: return "<unknown>" return "%s:%s" % (self.filename, self.lineno) +## +# Represent a section by inheriting from 'defaultdict'. +class OptionDict(collections.defaultdict): + + ## + # Init constructor. + # + # @param self the object itself + # @param config the "config" object -- typically a @a TalerConfig instance. + # @param section_name the section name to assign to this object. + def __init__(self, config, section_name: str) -> None: + self.config = weakref.ref(config) + self.section_name = section_name + super().__init__() + + ## + # Logic to run when a non-existent key is dereferenced. + # Just create and return a empty config @a Entry. Note + # that the freshly created entry will nonetheless put + # under the accessed key (that *does* become existent + # afterwards). + # + # @param self the object itself. + # @param key the key attempted to be accessed. + # @return the no-value entry. + def __missing__(self, key: str) -> Entry: + entry = Entry(self.config(), self.section_name, key) + self[key] = entry + return entry + + ## + # Attempt to fetch one value from the object. + # + # @param self the object itself. + # @param chunk the key (?) that is tried to access. + # @return the object, if it exists, or a freshly created + # (empty) one, if it doesn't exist. + def __getitem__(self, chunk: str) -> Entry: + return super().__getitem__(chunk.lower()) + + ## + # Set one value into the object. + # + # @param self the object itself. + # @param chunk key under which the value is going to be set. + # @param value value to set the @a chunk to. + def __setitem__(self, chunk: str, value: Entry) -> None: + super().__setitem__(chunk.lower(), value) + +## +# Collection of all the (@a OptionDict) sections. +class SectionDict(collections.defaultdict): + ## + # Automatically invoked when a missing section is + # dereferenced. It creates the missing - empty - section. + # + # @param self the object itself. + # @param key the dereferenced section name. + # @return the freshly created section. + def __missing__(self, key): + value = OptionDict(self, key) + self[key] = value + return value + + ## + # Attempt to retrieve a section. + # + # @param self the object itself. + # @param chunk the section name. + def __getitem__(self, chunk: str) -> OptionDict: + return super().__getitem__(chunk.lower()) + + ## + # Set a section. + # + # @param self the object itself. + # @param chunk the section name to set. + # @param value the value to set under that @a chunk. + def __setitem__(self, chunk: str, value: OptionDict) -> None: + super().__setitem__(chunk.lower(), value) + +## +# One loaded taler configuration, including base configuration +# files and included files. class TalerConfig: - """ - One loaded taler configuration, including base configuration - files and included files. - """ - def __init__(self): - """ - Initialize an empty configuration - """ - self.sections = SectionDict() - - # defaults != config file: the first is the 'base' - # whereas the second overrides things from the first. + + ## + # Init constructor.. + # + # @param self the object itself. + def __init__(self) -> None: + self.sections = SectionDict() # just plain dict + + ## + # Load a configuration file, instantiating a config object. + # + # @param filename the filename where to load the configuration + # from. If None, it defaults "taler.conf". + # @param load_defaults if True, then defaults values are loaded + # (from canonical directories like "<prefix>/share/config.d/taler/") + # before the actual configuration file. This latter then + # can override some/all the defaults. + # @return the config object. @staticmethod def from_file(filename=None, load_defaults=True): cfg = TalerConfig() @@ -204,22 +346,57 @@ class TalerConfig: filename = os.path.expanduser("~/.config/taler.conf") if load_defaults: cfg.load_defaults() - cfg.load_file(filename) + cfg.load_file(os.path.expanduser(filename)) return cfg - def value_string(self, section, option, **kwargs): + ## + # Get a string value from the config. + # + # @param self the config object itself. + # @param section the section to fetch the value from. + # @param option the value's option name. + # @param kwargs dict argument with instructions about + # the value retrieval logic. + # @return the wanted string (or a default / exception if + # a error occurs). + def value_string(self, section, option, **kwargs) -> str: return self.sections[section][option].value_string( kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) - def value_filename(self, section, option, **kwargs): + ## + # Get a value from the config that should be a filename. + # The variable expansion for the path's components is internally managed. + # + # @param self the config object itself. + # @param section the section to fetch the value from. + # @param option the value's option name. + # @param kwargs dict argument with instructions about + # the value retrieval logic. + # @return the wanted filename (or a default / exception if + # a error occurs). + def value_filename(self, section, option, **kwargs) -> str: return self.sections[section][option].value_filename( kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) - def value_int(self, section, option, **kwargs): + ## + # Get a integer value from the config. + # + # @param self the config object itself. + # @param section the section to fetch the value from. + # @param option the value's option name. + # @param kwargs dict argument with instructions about + # the value retrieval logic. + # @return the wanted integer (or a default / exception if + # a error occurs). + def value_int(self, section, option, **kwargs) -> int: return self.sections[section][option].value_int( kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) - def load_defaults(self): + ## + # Load default values from canonical locations. + # + # @param self the object itself. + def load_defaults(self) -> None: base_dir = os.environ.get("TALER_BASE_CONFIG") if base_dir: self.load_dir(base_dir) @@ -236,16 +413,25 @@ class TalerConfig: return LOGGER.warning("no base directory found") + ## + # Load configuration from environment variable + # TALER_CONFIG_FILE or from default location if the + # variable is not set. + # + # @param args currently unused. + # @param kwargs kwargs for subroutine @a from_file. + # @return freshly instantiated config object. @staticmethod def from_env(*args, **kwargs): - """ - Load configuration from environment variable TALER_CONFIG_FILE - or from default location if the variable is not set. - """ filename = os.environ.get("TALER_CONFIG_FILE") return TalerConfig.from_file(filename, *args, **kwargs) - def load_dir(self, dirname): + ## + # Load config values from _each_ file found in a directory. + # + # @param self the object itself. + # @param dirname the directory to crawl in the look for config files. + def load_dir(self, dirname) -> None: try: files = os.listdir(dirname) except FileNotFoundError: @@ -256,7 +442,11 @@ class TalerConfig: continue self.load_file(os.path.join(dirname, file)) - def load_file(self, filename): + ## + # Load config values from a file. + # + # @param filename config file to take the values from. + def load_file(self, filename) -> None: sections = self.sections try: with open(filename, "r") as file: @@ -271,6 +461,16 @@ class TalerConfig: if line.startswith("#"): # comment continue + if line.startswith("@INLINE@"): + pair = line.split() + if 2 != len(pair): + LOGGER.error("invalid inlined config filename given ('%s')" % line) + continue + if pair[1].startswith("/"): + self.load_file(pair[1]) + else: + self.load_file(os.path.join(os.path.dirname(filename), pair[1])) + continue if line.startswith("["): if not line.endswith("]"): LOGGER.error("invalid section header in line %s: %s", @@ -299,8 +499,16 @@ class TalerConfig: LOGGER.error("Configuration file (%s) not found", filename) sys.exit(3) - - def dump(self): + ## + # Dump the textual representation of a config object. + # + # Format: + # + # [section] + # option = value # FIXME (what is location?) + # + # @param self the object itself, that will be dumped. + def dump(self) -> None: for kv_section in self.sections.items(): print("[%s]" % (kv_section[1].section_name,)) for kv_option in kv_section[1].items(): @@ -309,7 +517,16 @@ class TalerConfig: kv_option[1].value, kv_option[1].location())) - def __getitem__(self, chunk): + + ## + # Return a whole section from this object. + # + # @param self the object itself. + # @param chunk name of the section to return. + # @return the section - note that if the section is + # not found, a empty one will created on the fly, + # then set under 'chunk', and returned. + def __getitem__(self, chunk: str) -> OptionDict: if isinstance(chunk, str): return self.sections[chunk] raise TypeError("index must be string") |