[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-taler-util] branch master updated: formatting, throw correct exce
From: |
gnunet |
Subject: |
[taler-taler-util] branch master updated: formatting, throw correct exception, version bump, more tests |
Date: |
Sun, 12 Jan 2020 19:32:04 +0100 |
This is an automated email from the git hooks/post-receive script.
dold pushed a commit to branch master
in repository taler-util.
The following commit(s) were added to refs/heads/master by this push:
new 7d15041 formatting, throw correct exception, version bump, more tests
7d15041 is described below
commit 7d15041f8a1aad859ed26fc7d6b5c7d882cfbc8c
Author: Florian Dold <address@hidden>
AuthorDate: Sun Jan 12 19:32:00 2020 +0100
formatting, throw correct exception, version bump, more tests
---
Makefile | 2 +-
setup.py | 2 +-
taler/util/amount.py | 29 ++++++++---
taler/util/gnunet_log.py | 45 +++++++++-------
taler/util/talerconfig.py | 130 +++++++++++++++++++++++++++++++---------------
tests/test_amount.py | 44 +++++++++++-----
tests/test_log.py | 21 +++++---
7 files changed, 182 insertions(+), 91 deletions(-)
diff --git a/Makefile b/Makefile
index 1768be0..8101927 100644
--- a/Makefile
+++ b/Makefile
@@ -36,7 +36,7 @@ check:
$(tox) || echo "error: you have to install tox"
pretty:
- $(find) . -type f -name '*.py' -or -name '*.py.in' -print0 | $(xargs)
-0 $(yapf) -i 2>&1 || true
+ black tests/ taler/
clean:
$(rm) -rf __pycache__ *~
diff --git a/setup.py b/setup.py
index 5b3c8d4..6794a14 100644
--- a/setup.py
+++ b/setup.py
@@ -25,7 +25,7 @@ with open('README', 'r') as f:
setup(
name='taler-util',
- version='0.6.3',
+ version='0.6.4',
license='LGPL3+',
platforms='any',
author='Taler Systems SA',
diff --git a/taler/util/amount.py b/taler/util/amount.py
index 9096f7d..3c11a43 100644
--- a/taler/util/amount.py
+++ b/taler/util/amount.py
@@ -23,26 +23,38 @@
from dataclasses import dataclass
from functools import total_ordering
+import re
+
class CurrencyMismatchError(Exception):
"""
Exception class to raise when an operation between two
amounts of different currency is being attempted.
"""
+
def __init__(self, curr1, curr2) -> None:
- super(CurrencyMismatchError, self).__init__(f"mismatched currency:
{curr1} vs {curr2}")
+ super(CurrencyMismatchError, self).__init__(
+ f"mismatched currency: {curr1} vs {curr2}"
+ )
+
class AmountOverflowError(Exception):
pass
+
+class AmountFormatError(Exception):
+ pass
+
+
MAX_AMOUNT_VALUE = 2 ** 52
FRACTIONAL_LENGTH = 8
FRACTIONAL_BASE = 1e8
+
@total_ordering
-class Amount():
+class Amount:
def __init__(self, currency, value, fraction):
if fraction >= FRACTIONAL_BASE:
raise AmountOverflowError("amount fraction too big")
@@ -72,13 +84,12 @@ class Amount():
@classmethod
def parse(cls, amount_str):
- exp = r'^\s*([-_*A-Za-z0-9]+):([0-9]+)\.?([0-9]+)?\s*$'
- import re
+ exp = r"^\s*([-_*A-Za-z0-9]+):([0-9]+)\.?([0-9]+)?\s*$"
parsed = re.search(exp, amount_str)
if not parsed:
- raise BadFormatAmount(amount_str)
+ raise AmountFormatError(f"invalid amount: {amount_str}")
- tail = ("." + (parsed.group(3) or "0"))
+ tail = "." + (parsed.group(3) or "0")
if len(tail) > FRACTIONAL_LENGTH + 1:
raise AmountOverflow()
@@ -90,7 +101,11 @@ class Amount():
def __add__(self, other):
if self.currency != other.currency:
raise CurrencyMismatchError(self.currency, other.currency)
- v = int(self.value + other.value + (self.fraction + other.fraction) //
FRACTIONAL_BASE)
+ v = int(
+ self.value
+ + other.value
+ + (self.fraction + other.fraction) // FRACTIONAL_BASE
+ )
if v >= MAX_AMOUNT_VALUE:
raise AmountOverflowError()
f = int((self.fraction + other.fraction) % FRACTIONAL_BASE)
diff --git a/taler/util/gnunet_log.py b/taler/util/gnunet_log.py
index b5847cf..a2336d6 100755
--- a/taler/util/gnunet_log.py
+++ b/taler/util/gnunet_log.py
@@ -68,12 +68,15 @@ class LogDefinition:
interval_re = "^([0-9]+)(-([0-9]+))?$"
match = re.match(interval_re, line_interval)
if match:
- return dict(min=int(match.group(1)),
- max=int(match.group(3) if match.group(3) else
match.group(1)))
+ return dict(
+ min=int(match.group(1)),
+ max=int(match.group(3) if match.group(3) else match.group(1)),
+ )
# just match every line if bad interval was provided.
return dict(min=0, max=float("inf"))
+
##
# Represent a loglevel.
#
@@ -150,7 +153,6 @@ class GnunetLogger:
fh = logging.FileHandler(filename)
self.logger.addHandler(fh)
-
##
# Parse the filename where to write log lines.
#
@@ -169,7 +171,6 @@ class GnunetLogger:
f = f.replace("%d", now.strftime("%d"))
return f
-
##
# Maps loglevels as strings, to loglevels as defined
# in _this_ object.
@@ -183,12 +184,12 @@ class GnunetLogger:
"ERROR": self.ERROR,
"WARNING": self.WARNING,
"INFO": self.INFO,
- "DEBUG": self.DEBUG}
+ "DEBUG": self.DEBUG,
+ }
# Defaults to INFO.
return level_map.get(level, self.INFO)
-
##
# Set the loglevel for this module.
def setup(self, loglevel):
@@ -218,11 +219,15 @@ class GnunetLogger:
# and non-given loglevel at object creation time)
for defi in self.definitions:
if defi.forced or not self.loglevel:
- if re.match(defi.component, self.component) \
- and re.match(defi.filename, filename) \
- and re.match(defi.function, function) \
- and defi.line_interval["min"] <= lineno <=
defi.line_interval["max"]:
-
self.logger.setLevel(level=self.string_to_loglevel(defi.loglevel).getLevel())
+ if (
+ re.match(defi.component, self.component)
+ and re.match(defi.filename, filename)
+ and re.match(defi.function, function)
+ and defi.line_interval["min"] <= lineno <=
defi.line_interval["max"]
+ ):
+ self.logger.setLevel(
+ level=self.string_to_loglevel(defi.loglevel).getLevel()
+ )
message_loglevel.getFunction()(message)
return
@@ -234,8 +239,7 @@ class GnunetLogger:
# Possibly, there also exists a default loglevel.
if self.loglevel:
- self.logger.setLevel(
- level=self.loglevel.getLevel())
+ self.logger.setLevel(level=self.loglevel.getLevel())
# (2) GNUNET_FORCE_LOG was NOT given and neither was
# a default loglevel, and also a unforced definition
@@ -246,7 +250,6 @@ class GnunetLogger:
message_loglevel.getFunction()(message)
-
##
# Helper function that parses definitions coming from the environment.
#
@@ -262,11 +265,13 @@ class GnunetLogger:
print("warning: GNUNET_(FORCE_)LOG is malformed")
return
- definition =
LogDefinition(gfl_split_split[GnunetLogger.COMPONENT_IDX],
-
gfl_split_split[GnunetLogger.FILENAME_IDX],
-
gfl_split_split[GnunetLogger.FUNCTION_IDX],
-
gfl_split_split[GnunetLogger.LINE_INTERVAL],
- gfl_split_split[GnunetLogger.LEVEL_IDX],
- forced)
+ definition = LogDefinition(
+ gfl_split_split[GnunetLogger.COMPONENT_IDX],
+ gfl_split_split[GnunetLogger.FILENAME_IDX],
+ gfl_split_split[GnunetLogger.FUNCTION_IDX],
+ gfl_split_split[GnunetLogger.LINE_INTERVAL],
+ gfl_split_split[GnunetLogger.LEVEL_IDX],
+ forced,
+ )
self.definitions.append(definition)
diff --git a/taler/util/talerconfig.py b/taler/util/talerconfig.py
index a0c2ce3..8427f33 100644
--- a/taler/util/talerconfig.py
+++ b/taler/util/talerconfig.py
@@ -38,6 +38,7 @@ TALER_DATADIR = None
try:
# not clear if this is a good idea ...
from talerpaths import TALER_DATADIR as t
+
TALER_DATADIR = t
except ImportError:
pass
@@ -47,12 +48,14 @@ except ImportError:
class ConfigurationError(Exception):
pass
+
##
# Exception class for malformed strings having with parameter
# expansion.
class ExpansionSyntaxError(Exception):
pass
+
##
# Do shell-style parameter expansion.
# Supported syntax:
@@ -84,7 +87,7 @@ def expand(var: str, getter: Callable[[str], str]) -> str:
end += 1
if balance != 0:
raise ExpansionSyntaxError("unbalanced parentheses")
- piece = var[start+2:end-1]
+ piece = var[start + 2 : end - 1]
if piece.find(":-") > 0:
varname, alt = piece.split(":-", 1)
replace = getter(varname)
@@ -97,9 +100,9 @@ def expand(var: str, getter: Callable[[str], str]) -> str:
replace = var[start:end]
else:
end = start + 2
- while end < len(var) and var[start+1:end+1].isalnum():
+ while end < len(var) and var[start + 1 : end + 1].isalnum():
end += 1
- varname = var[start+1:end]
+ varname = var[start + 1 : end]
replace = getter(varname)
if replace is None:
replace = var[start:end]
@@ -108,6 +111,7 @@ def expand(var: str, getter: Callable[[str], str]) -> str:
return result + var[pos:]
+
##
# A configuration entry.
class Entry:
@@ -139,8 +143,11 @@ class Entry:
# @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),)
+ return "<Entry section=%s, option=%s, value=%s>" % (
+ self.section,
+ self.option,
+ repr(self.value),
+ )
##
# Return the value for this entry, as is.
@@ -163,16 +170,26 @@ class Entry:
# @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()))
+ raise ConfigurationError(
+ "Missing required option '%s' in section '%s'"
+ % (self.option.upper(), self.section.upper())
+ )
if self.value is None:
if warn:
if default is not None:
- LOGGER.warning("Configuration is missing option '%s' in
section '%s',\
- falling back to '%s'", self.option,
self.section, default)
+ LOGGER.warning(
+ "Configuration is missing option '%s' in section '%s',\
+ falling back to '%s'",
+ self.option,
+ self.section,
+ default,
+ )
else:
- LOGGER.warning("Configuration ** is missing option '%s' in
section '%s'",
- self.option.upper(), self.section.upper())
+ LOGGER.warning(
+ "Configuration ** is missing option '%s' in section
'%s'",
+ self.option.upper(),
+ self.section.upper(),
+ )
return default
return self.value
@@ -192,8 +209,11 @@ class Entry:
try:
return int(value)
except ValueError:
- raise ConfigurationError("Expected number for option '%s' in
section '%s'" \
- % (self.option.upper(),
self.section.upper()))
+ raise ConfigurationError(
+ "Expected number for option '%s' in section '%s'"
+ % (self.option.upper(), self.section.upper())
+ )
+
##
# Fetch value to substitute to expansion variables.
#
@@ -235,6 +255,7 @@ class Entry:
return "<unknown>"
return "%s:%s" % (self.filename, self.lineno)
+
##
# Represent a section by inheriting from 'defaultdict'.
class OptionDict(collections.defaultdict):
@@ -284,6 +305,7 @@ class OptionDict(collections.defaultdict):
def __setitem__(self, chunk: str, value: Entry) -> None:
super().__setitem__(chunk.lower(), value)
+
##
# Collection of all the (@a OptionDict) sections.
class SectionDict(collections.defaultdict):
@@ -317,6 +339,7 @@ class SectionDict(collections.defaultdict):
def __setitem__(self, chunk: str, value: OptionDict) -> None:
super().__setitem__(chunk.lower(), value)
+
##
# One loaded taler configuration, including base configuration
# files and included files.
@@ -327,7 +350,7 @@ class TalerConfig:
#
# @param self the object itself.
def __init__(self) -> None:
- self.sections = SectionDict() # just plain dict
+ self.sections = SectionDict() # just plain dict
##
# Load a configuration file, instantiating a config object.
@@ -366,7 +389,8 @@ class TalerConfig:
# 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"))
+ kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")
+ )
##
# Get a value from the config that should be a filename.
@@ -381,7 +405,8 @@ class TalerConfig:
# 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"))
+ kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")
+ )
##
# Get a integer value from the config.
@@ -395,7 +420,8 @@ class TalerConfig:
# 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"))
+ kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")
+ )
##
# Load default values from canonical locations.
@@ -469,36 +495,57 @@ class TalerConfig:
if line.startswith("@INLINE@"):
pair = line.split()
if 2 != len(pair):
- LOGGER.error("invalid inlined config filename
given ('%s')" % line)
- continue
+ 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]))
+ 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",
- lineno, repr(line))
+ LOGGER.error(
+ "invalid section header in line %s: %s",
+ lineno,
+ repr(line),
+ )
section_name = line.strip("[]").strip().strip('"')
current_section = section_name
continue
if current_section is None:
- LOGGER.error("option outside of section in line %s:
%s", lineno, repr(line))
+ LOGGER.error(
+ "option outside of section in line %s: %s",
+ lineno,
+ repr(line),
+ )
continue
pair = line.split("=", 1)
if len(pair) != 2:
- LOGGER.error("invalid option in line %s: %s", lineno,
repr(line))
+ LOGGER.error(
+ "invalid option in line %s: %s", lineno, repr(line)
+ )
key = pair[0].strip()
value = pair[1].strip()
if value.startswith('"'):
value = value[1:]
if not value.endswith('"'):
- LOGGER.error("mismatched quotes in line %s: %s",
lineno, repr(line))
+ LOGGER.error(
+ "mismatched quotes in line %s: %s", lineno,
repr(line)
+ )
else:
value = value[:-1]
- entry = Entry(self.sections, current_section, key,
- value=value, filename=filename,
lineno=lineno)
+ entry = Entry(
+ self.sections,
+ current_section,
+ key,
+ value=value,
+ filename=filename,
+ lineno=lineno,
+ )
sections[current_section][key] = entry
except FileNotFoundError:
LOGGER.error("Configuration file (%s) not found", filename)
@@ -506,9 +553,9 @@ class TalerConfig:
##
# Dump the textual representation of a config object.
- #
+ #
# Format:
- #
+ #
# [section]
# option = value # FIXME (what is location?)
#
@@ -517,11 +564,10 @@ class TalerConfig:
for kv_section in list(self.sections.items()):
print("[%s]" % (kv_section[1].section_name,))
for kv_option in list(kv_section[1].items()):
- print("%s = %s # %s" % \
- (kv_option[1].option,
- kv_option[1].value,
- kv_option[1].location()))
-
+ print(
+ "%s = %s # %s"
+ % (kv_option[1].option, kv_option[1].value,
kv_option[1].location())
+ )
##
# Return a whole section from this object.
@@ -541,14 +587,14 @@ if __name__ == "__main__":
import argparse
PARSER = argparse.ArgumentParser()
- PARSER.add_argument("--section", "-s", dest="section",
- default=None, metavar="SECTION")
- PARSER.add_argument("--option", "-o", dest="option",
- default=None, metavar="OPTION")
- PARSER.add_argument("--config", "-c", dest="config",
- default=None, metavar="FILE")
- PARSER.add_argument("--filename", "-f", dest="expand_filename",
- default=False, action='store_true')
+ PARSER.add_argument(
+ "--section", "-s", dest="section", default=None, metavar="SECTION"
+ )
+ PARSER.add_argument("--option", "-o", dest="option", default=None,
metavar="OPTION")
+ PARSER.add_argument("--config", "-c", dest="config", default=None,
metavar="FILE")
+ PARSER.add_argument(
+ "--filename", "-f", dest="expand_filename", default=False,
action="store_true"
+ )
ARGS = PARSER.parse_args()
TC = TalerConfig.from_file(ARGS.config)
diff --git a/tests/test_amount.py b/tests/test_amount.py
index 3dd386a..559dd2c 100755
--- a/tests/test_amount.py
+++ b/tests/test_amount.py
@@ -19,32 +19,51 @@
# @version 0.0
# @repository https://git.taler.net/taler-util.git/
-from taler.util.amount import Amount, SignedAmount, AmountOverflowError,
MAX_AMOUNT_VALUE
+from taler.util.amount import (
+ Amount,
+ SignedAmount,
+ AmountFormatError,
+ AmountOverflowError,
+ MAX_AMOUNT_VALUE,
+)
from unittest import TestCase
import json
+
class TestAmount(TestCase):
def test_very_big_number(self):
with self.assertRaises(AmountOverflowError):
- self.Amount = Amount('TESTKUDOS',
-
value=99999999999999999999999999999999999999999999,
- fraction=0)
+ self.Amount = Amount(
+ "TESTKUDOS",
+ value=99999999999999999999999999999999999999999999,
+ fraction=0,
+ )
def test_add_overflow(self):
- a1 = Amount('TESTKUDOS',
- value=MAX_AMOUNT_VALUE,
- fraction=0)
+ a1 = Amount("TESTKUDOS", value=MAX_AMOUNT_VALUE, fraction=0)
with self.assertRaises(AmountOverflowError):
a2 = a1 + Amount.parse("TESTKUDOS:1")
def test_sub_overflow(self):
- a1 = Amount('TESTKUDOS',
- value=MAX_AMOUNT_VALUE,
- fraction=0)
+ a1 = Amount("TESTKUDOS", value=MAX_AMOUNT_VALUE, fraction=0)
s1 = SignedAmount(False, a1)
with self.assertRaises(AmountOverflowError):
s2 = s1 - SignedAmount.parse("TESTKUDOS:1")
+ def test_parse_error(self):
+ with self.assertRaises(AmountFormatError):
+ Amount.parse("TESTKUDOS:0,5")
+ with self.assertRaises(AmountFormatError):
+ Amount.parse("+TESTKUDOS:0.5")
+ with self.assertRaises(AmountFormatError):
+ Amount.parse("0.5")
+ with self.assertRaises(AmountFormatError):
+ Amount.parse(":0.5")
+ with self.assertRaises(AmountFormatError):
+ Amount.parse("EUR::0.5")
+ with self.assertRaises(AmountFormatError):
+ Amount.parse("EUR:.5")
+
def test_parse_and_cmp(self):
self.assertTrue(Amount.parse("EUR:0.0") < Amount.parse("EUR:0.5"))
@@ -52,7 +71,9 @@ class TestAmount(TestCase):
self.assertEqual(Amount.parse("TESTKUDOS:0").stringify(3),
"TESTKUDOS:0.000")
def test_signed_amount(self):
- self.assertEqual(SignedAmount.parse("TESTKUDOS:1.5").stringify(3),
"+TESTKUDOS:1.500")
+ self.assertEqual(
+ SignedAmount.parse("TESTKUDOS:1.5").stringify(3),
"+TESTKUDOS:1.500"
+ )
def test_zero_crossing(self):
p1 = SignedAmount.parse("EUR:1")
@@ -66,4 +87,3 @@ class TestAmount(TestCase):
self.assertEqual(p2 - p3, -p1)
self.assertEqual((-p2) + p3, p1)
-
diff --git a/tests/test_log.py b/tests/test_log.py
index 6409529..5d982d5 100755
--- a/tests/test_log.py
+++ b/tests/test_log.py
@@ -48,8 +48,6 @@ def clean_env():
del os.environ["GNUNET_FORCE_LOGFILE"]
-
-
##
# "mother" class of all the tests. NOTE: no logs will appear
# on screen, as the setLevel function is mocked (therefore the
@@ -82,7 +80,10 @@ class TestGnunetLog(TestCase):
gl.log("msg", gl.DEBUG)
today = datetime.now()
- expected_filename = "/tmp/gnunet-pylog-%s-%s.log" % (str(os.getpid()),
today.strftime("%Y_%m_%d"))
+ expected_filename = "/tmp/gnunet-pylog-%s-%s.log" % (
+ str(os.getpid()),
+ today.strftime("%Y_%m_%d"),
+ )
mocked_FileHandler.assert_called_with(expected_filename)
mocked_addHandler.assert_called_with(unused_mock)
@@ -107,7 +108,6 @@ class TestGnunetLog(TestCase):
gl.log("msg", gl.DEBUG)
mocked_setLevel.assert_called_with(level=logging.INFO)
-
##
# This function tests the case where *only* the GNUNET_LOG
# env variable is set -- not even the manual setup of the
@@ -122,7 +122,9 @@ class TestGnunetLog(TestCase):
@patch("logging.basicConfig")
def test_non_forced_env(self, mocked_basicConfig, mocked_setLevel):
self.assertIsNone(os.environ.get("GNUNET_FORCE_LOG"))
- os.environ["GNUNET_LOG"] =
"gnunet-pylog;log_test.py;test_non_forced_env;99;ERROR" # lineno is not 100%
accurate.
+ os.environ[
+ "GNUNET_LOG"
+ ] = "gnunet-pylog;log_test.py;test_non_forced_env;99;ERROR" # lineno
is not 100% accurate.
gl = GL("gnunet-pylog")
gl.log("msg", gl.DEBUG)
mocked_setLevel.assert_called_with(level=logging.INFO)
@@ -141,7 +143,9 @@ class TestGnunetLog(TestCase):
@patch("logging.basicConfig")
def test_only_forced_env(self, mocked_basicConfig, mocked_setLevel):
self.assertIsNone(os.environ.get("GNUNET_LOG"))
- os.environ["GNUNET_FORCE_LOG"] =
"gnunet-pylog;log_test.py;test_only_forced_env;90-200;ERROR"
+ os.environ[
+ "GNUNET_FORCE_LOG"
+ ] = "gnunet-pylog;log_test.py;test_only_forced_env;90-200;ERROR"
gl = GL("gnunet-pylog")
gl.log("msg", gl.DEBUG)
mocked_setLevel.assert_called_with(level=logging.INFO)
@@ -165,7 +169,6 @@ class TestGnunetLog(TestCase):
gl.log("msg", gl.DEBUG)
mocked_setLevel.assert_called_with(level=logging.ERROR)
-
##
# This function tests the case where *both* the manual loglevel
# and the forced env variable are setup; the expected result is
@@ -204,7 +207,9 @@ class TestGnunetLog(TestCase):
# the real setLevel.
@patch("logging.Logger.setLevel")
@patch("logging.basicConfig")
- def test_manual_loglevel_AND_nonforced_env(self, mocked_basicConfig,
mocked_setLevel):
+ def test_manual_loglevel_AND_nonforced_env(
+ self, mocked_basicConfig, mocked_setLevel
+ ):
self.assertIsNone(os.environ.get("GNUNET_LOG"))
self.assertIsNone(os.environ.get("GNUNET_FORCE_LOG"))
os.environ["GNUNET_LOG"] = ";;;;DEBUG"
--
To stop receiving notification emails like this one, please contact
address@hidden.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-taler-util] branch master updated: formatting, throw correct exception, version bump, more tests,
gnunet <=