[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[GNUnet-SVN] [taler-survey] branch master updated (13b2e2c -> 450921c)
From: |
gnunet |
Subject: |
[GNUnet-SVN] [taler-survey] branch master updated (13b2e2c -> 450921c) |
Date: |
Tue, 05 Dec 2017 18:09:03 +0100 |
This is an automated email from the git hooks/post-receive script.
marcello pushed a change to branch master
in repository survey.
from 13b2e2c more text fixes
new fc348c3 use linted talerconfig
new 450921c linting all but 'app' global name (enforced by Flask)
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
talersurvey/survey/amount.py | 74 ++++++------
talersurvey/survey/survey.py | 71 +++++------
talersurvey/talerconfig.py | 277 +++++++++++++++++++++++--------------------
talersurvey/tests.py | 6 +-
4 files changed, 227 insertions(+), 201 deletions(-)
diff --git a/talersurvey/survey/amount.py b/talersurvey/survey/amount.py
index 9a6318a..46e3446 100644
--- a/talersurvey/survey/amount.py
+++ b/talersurvey/survey/amount.py
@@ -23,43 +23,46 @@
# which might need it.
class CurrencyMismatch(Exception):
- pass
+ def __init__(self, curr1, curr2):
+ super(CurrencyMismatch, self).__init__(
+ "%s vs %s" % (curr1, curr2))
class BadFormatAmount(Exception):
def __init__(self, faulty_str):
- self.faulty_str = faulty_str
+ super(BadFormatAmount, self).__init__(
+ "Bad format amount: " + faulty_str)
class Amount:
# How many "fraction" units make one "value" unit of currency
# (Taler requires 10^8). Do not change this 'constant'.
@staticmethod
- def FRACTION():
+ def _fraction():
return 10 ** 8
@staticmethod
- def MAX_VALUE():
+ def _max_value():
return (2 ** 53) - 1
def __init__(self, currency, value=0, fraction=0):
# type: (str, int, int) -> Amount
- assert(value >= 0 and fraction >= 0)
+ assert value >= 0 and fraction >= 0
self.value = value
self.fraction = fraction
self.currency = currency
self.__normalize()
- assert(self.value <= Amount.MAX_VALUE())
+ assert self.value <= Amount._max_value()
# Normalize amount
def __normalize(self):
- if self.fraction >= Amount.FRACTION():
- self.value += int(self.fraction / Amount.FRACTION())
- self.fraction = self.fraction % Amount.FRACTION()
+ if self.fraction >= Amount._fraction():
+ self.value += int(self.fraction / Amount._fraction())
+ self.fraction = self.fraction % Amount._fraction()
# Parse a string matching the format "A:B.C"
# instantiating an amount object.
@classmethod
def parse(cls, amount_str):
- exp = '^\s*([-_*A-Za-z0-9]+):([0-9]+)\.([0-9]+)\s*$'
+ exp = r'^\s*([-_*A-Za-z0-9]+):([0-9]+)\.([0-9]+)\s*$'
import re
parsed = re.search(exp, amount_str)
if not parsed:
@@ -67,7 +70,7 @@ class Amount:
value = int(parsed.group(2))
fraction = 0
for i, digit in enumerate(parsed.group(3)):
- fraction += int(int(digit) * (Amount.FRACTION() / 10 ** (i+1)))
+ fraction += int(int(digit) * (Amount._fraction() / 10 ** (i+1)))
return cls(parsed.group(1), value, fraction)
# Comare two amounts, return:
@@ -75,16 +78,16 @@ class Amount:
# 0 if a == b
# 1 if a > b
@staticmethod
- def cmp(a, b):
- if a.currency != b.currency:
- raise CurrencyMismatch()
- if a.value == b.value:
- if a.fraction < b.fraction:
+ def cmp(am1, am2):
+ if am1.currency != am2.currency:
+ raise CurrencyMismatch(am1.currency, am2.currency)
+ if am1.value == am2.value:
+ if am1.fraction < am2.fraction:
return -1
- if a.fraction > b.fraction:
+ if am1.fraction > am2.fraction:
return 1
return 0
- if a.value < b.value:
+ if am1.value < am2.value:
return -1
return 1
@@ -94,34 +97,35 @@ class Amount:
self.fraction = fraction
# Add the given amount to this one
- def add(self, a):
- if self.currency != a.currency:
- raise CurrencyMismatch()
- self.value += a.value
- self.fraction += a.fraction
+ def add(self, amount):
+ if self.currency != amount.currency:
+ raise CurrencyMismatch(self.currency, amount.currency)
+ self.value += amount.value
+ self.fraction += amount.fraction
self.__normalize()
# Subtract passed amount from this one
- def subtract(self, a):
- if self.currency != a.currency:
- raise CurrencyMismatch()
- if self.fraction < a.fraction:
- self.fraction += Amount.FRACTION()
+ def subtract(self, amount):
+ if self.currency != amount.currency:
+ raise CurrencyMismatch(self.currency, amount.currency)
+ if self.fraction < amount.fraction:
+ self.fraction += Amount._fraction()
self.value -= 1
- if self.value < a.value:
+ if self.value < amount.value:
raise ValueError('self is lesser than amount to be subtracted')
- self.value -= a.value
- self.fraction -= a.fraction
+ self.value -= amount.value
+ self.fraction -= amount.fraction
# Dump string from this amount, will put 'ndigits' numbers
# after the dot.
def stringify(self, ndigits):
assert ndigits > 0
ret = '%s:%s.' % (self.currency, str(self.value))
- f = self.fraction
- for i in range(0, ndigits):
- ret += str(int(f / (Amount.FRACTION() / 10)))
- f = (f * 10) % (Amount.FRACTION())
+ fraction = self.fraction
+ while ndigits > 0:
+ ret += str(int(fraction / (Amount._fraction() / 10)))
+ fraction = (fraction * 10) % (Amount._fraction())
+ ndigits -= 1
return ret
# Dump the Taler-compliant 'dict' amount
diff --git a/talersurvey/survey/survey.py b/talersurvey/survey/survey.py
index d44db73..80b07d1 100644
--- a/talersurvey/survey/survey.py
+++ b/talersurvey/survey/survey.py
@@ -14,53 +14,53 @@
#
# @author Marcello Stanisci
-import flask
import os
import base64
-import requests
import logging
import json
-from .amount import Amount
-from talersurvey.talerconfig import TalerConfig
from urllib.parse import urljoin
+import flask
+import requests
+from talersurvey.talerconfig import TalerConfig
+from .amount import Amount
-
-base_dir = os.path.dirname(os.path.abspath(__file__))
-app = flask.Flask(__name__, template_folder=base_dir)
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+app = flask.Flask(__name__, template_folder=BASE_DIR)
app.debug = True
app.secret_key = base64.b64encode(os.urandom(64)).decode('utf-8')
-tc = TalerConfig.from_env()
-BACKEND_URL = tc["frontends"]["backend"].value_string(required=True)
-CURRENCY = tc["taler"]["currency"].value_string(required=True)
+TC = TalerConfig.from_env()
+BACKEND_URL = TC["frontends"]["backend"].value_string(required=True)
+CURRENCY = TC["taler"]["currency"].value_string(required=True)
app.config.from_object(__name__)
-logger = logging.getLogger(__name__)
+LOGGER = logging.getLogger(__name__)
def backend_error(requests_response):
- logger.error("Backend error: status code: "
+ LOGGER.error("Backend error: status code: "
+ str(requests_response.status_code))
try:
return flask.jsonify(requests_response.json()),
requests_response.status_code
except json.decoder.JSONDecodeError:
- logger.error("Backend error (NO JSON returned): status code: "
+ LOGGER.error("Backend error (NO JSON returned): status code: "
+ str(requests_response.status_code))
return flask.jsonify(dict(error="Backend died, no JSON got from it")),
502
@app.context_processor
def utility_processor():
+
def join_urlparts(*parts):
- s = ""
- i = 0
- while i < len(parts):
- n = parts[i]
- i += 1
- if s.endswith("/"):
- n = n.lstrip("/")
- elif s and not n.startswith("/"):
- n = "/" + n
- s += n
- return s
+ ret = ""
+ part = 0
+ while part < len(parts):
+ buf = parts[part]
+ part += 1
+ if ret.endswith("/"):
+ buf = buf.lstrip("/")
+ elif ret and not buf.startswith("/"):
+ buf = "/" + buf
+ ret += buf
+ return ret
def url(my_url):
return join_urlparts(flask.request.script_root, my_url)
@@ -72,11 +72,11 @@ def utility_processor():
@app.route("/tip-pickup", methods=["POST"])
def pick():
body = flask.request.get_json()
- r = requests.post(urljoin(BACKEND_URL, 'tip-pickup'), json=body)
- if 200 != r.status_code:
- return backend_error(r)
- return flask.jsonify(r.json())
-
+ resp = requests.post(urljoin(BACKEND_URL, 'tip-pickup'),
+ json=body)
+ if resp.status_code != 200:
+ return backend_error(resp)
+ return flask.jsonify(resp.json())
@app.route("/submit-survey", methods=["POST"])
def submit_survey():
@@ -84,14 +84,15 @@ def submit_survey():
amount=Amount(CURRENCY, 1).dump(),
instance="default",
justification="Payment methods survey")
- r = requests.post(urljoin(BACKEND_URL, 'tip-authorize'), json=tip_spec)
- if 200 != r.status_code:
- return backend_error(r)
+ resp = requests.post(urljoin(BACKEND_URL, 'tip-authorize'),
+ json=tip_spec)
+ if resp.status_code != 200:
+ return backend_error(resp)
response = flask.make_response(
- flask.render_template("templates/wait.html", success=True),
- 402)
- response.headers["X-Taler-Tip"] = r.json()["tip_token"]
+ flask.render_template("templates/wait.html", success=True),
+ 402)
+ response.headers["X-Taler-Tip"] = resp.json()["tip_token"]
return response
diff --git a/talersurvey/talerconfig.py b/talersurvey/talerconfig.py
index ba4dfbb..a7ca065 100644
--- a/talersurvey/talerconfig.py
+++ b/talersurvey/talerconfig.py
@@ -18,23 +18,22 @@
Parse GNUnet-style configurations in pure Python
"""
-# FIXME: make sure that autovivification of config entries
-# does not leave garbage behind (use weakrefs!)
-
import logging
import collections
import os
import weakref
+import sys
+import re
-logger = logging.getLogger(__name__)
+LOGGER = logging.getLogger(__name__)
__all__ = ["TalerConfig"]
-taler_datadir = None
+TALER_DATADIR = None
try:
# not clear if this is a good idea ...
- from talerpaths import taler_datadir as t
- taler_datadir = t
+ from talerpaths import TALER_DATADIR as t
+ TALER_DATADIR = t
except ImportError:
pass
@@ -45,7 +44,7 @@ class ExpansionSyntaxError(Exception):
pass
-def expand(s, getter):
+def expand(var, getter):
"""
Do shell-style parameter expansion.
Supported syntax:
@@ -56,18 +55,18 @@ def expand(s, getter):
pos = 0
result = ""
while pos != -1:
- start = s.find("$", pos)
+ start = var.find("$", pos)
if start == -1:
break
- if s[start:].startswith("${"):
+ if var[start:].startswith("${"):
balance = 1
end = start + 2
- while balance > 0 and end < len(s):
- balance += {"{": 1, "}": -1}.get(s[end], 0)
+ while balance > 0 and end < len(var):
+ balance += {"{": 1, "}": -1}.get(var[end], 0)
end += 1
if balance != 0:
raise ExpansionSyntaxError("unbalanced parentheses")
- piece = s[start+2:end-1]
+ piece = var[start+2:end-1]
if piece.find(":-") > 0:
varname, alt = piece.split(":-", 1)
replace = getter(varname)
@@ -77,20 +76,20 @@ def expand(s, getter):
varname = piece
replace = getter(varname)
if replace is None:
- replace = s[start:end]
+ replace = var[start:end]
else:
end = start + 2
- while end < len(s) and s[start+1:end+1].isalnum():
+ while end < len(var) and var[start+1:end+1].isalnum():
end += 1
- varname = s[start+1:end]
+ varname = var[start+1:end]
replace = getter(varname)
if replace is None:
- replace = s[start:end]
+ replace = var[start:end]
result = result + replace
pos = end
- return result + s[pos:]
+ return result + var[pos:]
class OptionDict(collections.defaultdict):
@@ -99,79 +98,81 @@ class OptionDict(collections.defaultdict):
self.section_name = section_name
super().__init__()
def __missing__(self, key):
- e = Entry(self.config(), self.section_name, key)
- self[key] = e
- return e
- def __getitem__(self, slice):
- return super().__getitem__(slice.lower())
- def __setitem__(self, slice, value):
- super().__setitem__(slice.lower(), value)
+ 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 __init__(self):
- super().__init__()
def __missing__(self, key):
- v = OptionDict(self, key)
- self[key] = v
- return v
- def __getitem__(self, slice):
- return super().__getitem__(slice.lower())
- def __setitem__(self, slice, value):
- super().__setitem__(slice.lower(), value)
+ 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)
class Entry:
- def __init__(self, config, section, option, value=None, filename=None,
lineno=None):
- self.value = value
- self.filename = filename
- self.lineno = lineno
+ def __init__(self, config, section, option, **kwargs):
+ self.value = kwargs.get("value")
+ self.filename = kwargs.get("filename")
+ self.lineno = kwargs.get("lineno")
self.section = section
self.option = option
self.config = weakref.ref(config)
def __repr__(self):
- 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),)
def __str__(self):
return self.value
- def value_string(self, default=None, warn=False, required=False):
+ def value_string(self, default=None, required=False, warn=False):
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.warn("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.warn("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
- def value_int(self, default=None, warn=False, required=False):
- v = self.value_string(default, warn, required)
- if v is None:
+ def value_int(self, default=None, required=False, warn=False):
+ value = self.value_string(default, warn, required)
+ if value is None:
return None
try:
- return int(v)
+ 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()))
def _getsubst(self, key):
- x = self.config()["paths"][key].value
- if x is not None:
- return x
- x = os.environ.get(key)
- if x is not None:
- return x
+ value = self.config()["paths"][key].value
+ if value is not None:
+ return value
+ value = os.environ.get(key)
+ if value is not None:
+ return value
return None
- def value_filename(self, default=None, warn=False, required=False):
- v = self.value_string(default, warn, required)
- if v is None:
+ def value_filename(self, default=None, required=False, warn=False):
+ value = self.value_string(default, required, warn)
+ if value is None:
return None
- return expand(v, lambda x: self._getsubst(x))
+ return expand(value, self._getsubst)
def location(self):
if self.filename is None or self.lineno is None:
@@ -190,6 +191,8 @@ class TalerConfig:
"""
self.sections = SectionDict()
+ # defaults != config file: the first is the 'base'
+ # whereas the second overrides things from the first.
@staticmethod
def from_file(filename=None, load_defaults=True):
cfg = TalerConfig()
@@ -204,14 +207,17 @@ class TalerConfig:
cfg.load_file(filename)
return cfg
- def value_string(self, section, option, default=None, required=None,
warn=False):
- return self.sections[section][option].value_string(default, required,
warn)
+ def value_string(self, section, option, **kwargs):
+ return self.sections[section][option].value_string(
+ kwargs.get("default"), kwargs.get("required"), kwargs.get("warn"))
- def value_filename(self, section, option, default=None, required=None,
warn=False):
- return self.sections[section][option].value_filename(default,
required, warn)
+ def value_filename(self, section, option, **kwargs):
+ return self.sections[section][option].value_filename(
+ kwargs.get("default"), kwargs.get("required"), kwargs.get("warn"))
- def value_int(self, section, option, default=None, required=None,
warn=False):
- return self.sections[section][option].value_int(default, required,
warn)
+ def value_int(self, section, option, **kwargs):
+ return self.sections[section][option].value_int(
+ kwargs.get("default"), kwargs.get("required"), kwargs.get("warn"))
def load_defaults(self):
base_dir = os.environ.get("TALER_BASE_CONFIG")
@@ -220,12 +226,15 @@ class TalerConfig:
return
prefix = os.environ.get("TALER_PREFIX")
if prefix:
+ tmp = os.path.split(os.path.normpath(prefix))
+ if re.match("lib", tmp[1]):
+ prefix = tmp[0]
self.load_dir(os.path.join(prefix, "share/taler/config.d"))
return
- if taler_datadir:
- self.load_dir(os.path.join(taler_datadir, "share/taler/config.d"))
+ if TALER_DATADIR:
+ self.load_dir(os.path.join(TALER_DATADIR, "share/taler/config.d"))
return
- logger.warn("no base directory found")
+ LOGGER.warning("no base directory found")
@staticmethod
def from_env(*args, **kwargs):
@@ -240,7 +249,7 @@ class TalerConfig:
try:
files = os.listdir(dirname)
except FileNotFoundError:
- logger.warn("can't read config directory '%s'", dirname)
+ LOGGER.warning("can't read config directory '%s'", dirname)
return
for file in files:
if not file.endswith(".conf"):
@@ -249,73 +258,85 @@ class TalerConfig:
def load_file(self, filename):
sections = self.sections
- with open(filename, "r") as file:
- lineno = 0
- current_section = None
- for line in file:
- lineno += 1
- line = line.strip()
- if len(line) == 0:
- # empty line
- continue
- if line.startswith("#"):
- # comment
- continue
- if line.startswith("["):
- if not line.endswith("]"):
- 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))
- continue
- kv = line.split("=", 1)
- if len(kv) != 2:
- logger.error("invalid option in line %s: %s", lineno,
repr(line))
- key = kv[0].strip()
- value = kv[1].strip()
- if value.startswith('"'):
- value = value[1:]
- if not value.endswith('"'):
- logger.error("mismatched quotes in line %s: %s",
lineno, repr(line))
- else:
- value = value[:-1]
- e = Entry(self.sections, current_section, key, value,
filename, lineno)
- sections[current_section][key] = e
+ try:
+ with open(filename, "r") as file:
+ lineno = 0
+ current_section = None
+ for line in file:
+ lineno += 1
+ line = line.strip()
+ if line == "":
+ # empty line
+ continue
+ if line.startswith("#"):
+ # comment
+ continue
+ if line.startswith("["):
+ if not line.endswith("]"):
+ 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))
+ continue
+ pair = line.split("=", 1)
+ if len(pair) != 2:
+ 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))
+ else:
+ value = value[:-1]
+ 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)
+ sys.exit(3)
def dump(self):
- for section_name, section in self.sections.items():
- print("[%s]" % (section.section_name,))
- for option_name, e in section.items():
- print("%s = %s # %s" % (e.option, e.value, e.location()))
-
- def __getitem__(self, slice):
- if isinstance(slice, str):
- return self.sections[slice]
+ for kv_section in self.sections.items():
+ print("[%s]" % (kv_section[1].section_name,))
+ for kv_option in kv_section[1].items():
+ print("%s = %s # %s" % \
+ (kv_option[1].option,
+ kv_option[1].value,
+ kv_option[1].location()))
+
+ def __getitem__(self, chunk):
+ if isinstance(chunk, str):
+ return self.sections[chunk]
raise TypeError("index must be string")
if __name__ == "__main__":
- import sys
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')
- args = parser.parse_args()
-
- tc = TalerConfig.from_file(args.config)
-
- if args.section is not None and args.option is not None:
- if args.expand_filename:
- x = tc.value_filename(args.section, args.option)
+ 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')
+ ARGS = PARSER.parse_args()
+
+ TC = TalerConfig.from_file(ARGS.config)
+
+ if ARGS.section is not None and ARGS.option is not None:
+ if ARGS.expand_filename:
+ X = TC.value_filename(ARGS.section, ARGS.option)
else:
- x = tc.value_string(args.section, args.option)
- if x is not None:
- print(x)
+ X = TC.value_string(ARGS.section, ARGS.option)
+ if X is not None:
+ print(X)
else:
- tc.dump()
+ TC.dump()
diff --git a/talersurvey/tests.py b/talersurvey/tests.py
index 56e66e4..d5845bc 100644
--- a/talersurvey/tests.py
+++ b/talersurvey/tests.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
import unittest
-from talersurvey.survey import survey
from mock import patch, MagicMock
+from talersurvey.survey import survey
from talersurvey.talerconfig import TalerConfig
TC = TalerConfig.from_env()
@@ -27,7 +27,7 @@ class SurveyTestCase(unittest.TestCase):
mocked_post.assert_called_with(
"http://backend.test.taler.net/tip-authorize",
json={
- "pickup_url": "http://localhost/tip-pickup",
+ "pickup_url": "http://localhost/tip-pickup",
"amount": {
"value": 1,
"fraction": 0,
@@ -46,7 +46,7 @@ class SurveyTestCase(unittest.TestCase):
content_type="application/json")
mocked_post.assert_called_with(
"http://backend.test.taler.net/tip-pickup",
- json={})
+ json={})
if __name__ == "__main__":
unittest.main()
--
To stop receiving notification emails like this one, please contact
address@hidden
- [GNUnet-SVN] [taler-survey] branch master updated (13b2e2c -> 450921c),
gnunet <=