summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.style.yapf5
-rw-r--r--Makefile4
-rw-r--r--talerblog/blog/blog.py165
-rw-r--r--talerblog/blog/content.py11
-rw-r--r--talerblog/talerconfig.py112
-rw-r--r--talerblog/tests.py15
6 files changed, 212 insertions, 100 deletions
diff --git a/.style.yapf b/.style.yapf
new file mode 100644
index 0000000..3b39780
--- /dev/null
+++ b/.style.yapf
@@ -0,0 +1,5 @@
+[style]
+based_on_style = pep8
+coalesce_brackets=True
+column_limit=80
+dedent_closing_brackets=True
diff --git a/Makefile b/Makefile
index e2a4bfb..d40e2f4 100644
--- a/Makefile
+++ b/Makefile
@@ -34,3 +34,7 @@ clean:
.PHONY: dist
dist:
git archive --format=tar.gz HEAD -o taler-merchant-blog.tar.gz
+
+.PHONY: pretty
+pretty:
+ yapf -r -i talerblog/
diff --git a/talerblog/blog/blog.py b/talerblog/blog/blog.py
index ba10453..0b56027 100644
--- a/talerblog/blog/blog.py
+++ b/talerblog/blog/blog.py
@@ -33,7 +33,6 @@ from cachelib import UWSGICache, SimpleCache
from talerblog.talerconfig import TalerConfig
from ..blog.content import ARTICLES, get_article_file, get_image_file
-
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
app = flask.Flask(__name__, template_folder=BASE_DIR)
app.secret_key = base64.b64encode(os.urandom(64)).decode('utf-8')
@@ -59,6 +58,7 @@ def utility_processor():
# These helpers will be available in templates
def env(name, default=None):
return os.environ.get(name, default)
+
return dict(env=env)
@@ -71,6 +71,7 @@ def err_abort(abort_status_code, **params):
t = flask.render_template("templates/error.html", **params)
flask.abort(flask.make_response(t, abort_status_code))
+
##
# Issue a GET request to the backend.
#
@@ -81,7 +82,9 @@ def err_abort(abort_status_code, **params):
def backend_get(endpoint, params):
headers = {"Authorization": "ApiKey " + APIKEY}
try:
- resp = requests.get(urljoin(BACKEND_URL, endpoint), params=params, headers=headers)
+ resp = requests.get(
+ urljoin(BACKEND_URL, endpoint), params=params, headers=headers
+ )
except requests.ConnectionError:
err_abort(500, message="Could not establish connection to backend")
try:
@@ -89,10 +92,15 @@ def backend_get(endpoint, params):
except ValueError:
err_abort(500, message="Could not parse response from backend")
if resp.status_code != 200:
- err_abort(500, message="Backend returned error status",
- json=response_json, status_code=resp.status_code)
+ err_abort(
+ 500,
+ message="Backend returned error status",
+ json=response_json,
+ status_code=resp.status_code
+ )
return response_json
+
##
# POST a request to the backend, and return a error
# response if any error occurs.
@@ -104,21 +112,29 @@ def backend_get(endpoint, params):
def backend_post(endpoint, json):
headers = {"Authorization": "ApiKey " + APIKEY}
try:
- resp = requests.post(urljoin(BACKEND_URL, endpoint), json=json, headers=headers)
+ resp = requests.post(
+ urljoin(BACKEND_URL, endpoint), json=json, headers=headers
+ )
except requests.ConnectionError:
err_abort(500, message="Could not establish connection to backend")
try:
response_json = resp.json()
except ValueError:
- err_abort(500, message="Could not parse response from backend",
- status_code=resp.status_code)
+ err_abort(
+ 500,
+ message="Could not parse response from backend",
+ status_code=resp.status_code
+ )
if resp.status_code != 200:
- err_abort(500, message="Backend returned error status",
- json=response_json, status_code=resp.status_code)
+ err_abort(
+ 500,
+ message="Backend returned error status",
+ json=response_json,
+ status_code=resp.status_code
+ )
return response_json
-
##
# "Fallback" exception handler to capture all the unmanaged errors.
#
@@ -127,9 +143,12 @@ def backend_post(endpoint, json):
# (and execution stack!).
@app.errorhandler(Exception)
def internal_error(e):
- return flask.render_template("templates/error.html",
- message="Internal error",
- stack=traceback.format_exc())
+ return flask.render_template(
+ "templates/error.html",
+ message="Internal error",
+ stack=traceback.format_exc()
+ )
+
##
# Serve the main index page.
@@ -137,9 +156,12 @@ def internal_error(e):
# @return response object of the index page.
@app.route("/")
def index():
- return flask.render_template("templates/index.html",
- merchant_currency=CURRENCY,
- articles=ARTICLES.values())
+ return flask.render_template(
+ "templates/index.html",
+ merchant_currency=CURRENCY,
+ articles=ARTICLES.values()
+ )
+
##
# Serve the "/javascript" page.
@@ -167,7 +189,7 @@ except ImportError:
#
# @param order_id the order ID of the transaction to refund.
# @return the following errors (named by HTTP response code):
-# - 400: no article was asked to be refunded!
+# - 400: no article was asked to be refunded!
# - 401: the refund was asked on a non-payed article.
# - 500: the backend was unable to give response.
# Or, in the successful case, a redirection to the
@@ -180,11 +202,15 @@ def refund(order_id):
return flask.jsonify(dict(error="No article_name found in form")), 400
LOGGER.info("Looking for %s to refund" % article_name)
if not order_id:
- return flask.jsonify(dict(error="Aborting refund: article not payed")), 401
- refund_spec = dict(instance=INSTANCE,
- order_id=order_id,
- reason="Demo reimbursement",
- refund=ARTICLE_AMOUNT)
+ return flask.jsonify(
+ dict(error="Aborting refund: article not payed")
+ ), 401
+ refund_spec = dict(
+ instance=INSTANCE,
+ order_id=order_id,
+ reason="Demo reimbursement",
+ refund=ARTICLE_AMOUNT
+ )
resp = backend_post("refund", refund_spec)
try:
# delete from paid article cache
@@ -193,9 +219,12 @@ def refund(order_id):
paid_articles_cache.delete(session_id + "-" + article_name)
return flask.redirect(resp["refund_redirect_url"])
except KeyError:
- err_abort(500, message="Response from backend incomplete",
- json=resp, stack=traceback.format_exc())
-
+ err_abort(
+ 500,
+ message="Response from backend incomplete",
+ json=resp,
+ stack=traceback.format_exc()
+ )
##
@@ -214,19 +243,25 @@ def refund(order_id):
def render_article(article_name, data, order_id):
article_info = ARTICLES.get(article_name)
if article_info is None:
- m = "Internal error: Files for article ({}) not found.".format(article_name)
+ m = "Internal error: Files for article ({}) not found.".format(
+ article_name
+ )
err_abort(500, message=m)
if data is not None:
if data in article_info.extra_files:
return flask.send_file(get_image_file(data))
m = "Supplemental file ({}) for article ({}) not found.".format(
- data, article_name)
+ data, article_name
+ )
err_abort(404, message=m)
# the order_id is needed for refunds
- return flask.render_template("templates/article_frame.html",
- article_file=get_article_file(article_info),
- article_name=article_name,
- order_id=order_id)
+ return flask.render_template(
+ "templates/article_frame.html",
+ article_file=get_article_file(article_info),
+ article_name=article_name,
+ order_id=order_id
+ )
+
def get_qrcode_svg(data):
factory = qrcode.image.svg.SvgImage
@@ -239,12 +274,13 @@ def get_qrcode_svg(data):
# to check if the payment has been completed via the QR code.
@app.route("/check-status/<order_id>/<session_id>")
def check_status(order_id, session_id):
- pay_params = dict(instance=INSTANCE,
- order_id=order_id,
- session_id=session_id)
+ pay_params = dict(
+ instance=INSTANCE, order_id=order_id, session_id=session_id
+ )
pay_status = backend_get("check-payment", pay_params)
return flask.jsonify(paid=pay_status["paid"])
+
##
# Trigger a article purchase. The logic follows the main steps:
#
@@ -289,19 +325,21 @@ def article(article_name, data=None):
extra=dict(article_name=article_name),
fulfillment_url=flask.request.base_url,
instance=INSTANCE,
- summary="Essay: " + article_name.replace("_", " "))
+ summary="Essay: " + article_name.replace("_", " ")
+ )
order_resp = backend_post("order", dict(order=order))
order_id = order_resp["order_id"]
return flask.redirect(
- flask.url_for("article",
- article_name=article_name,
- order_id=order_id))
+ flask.url_for(
+ "article", article_name=article_name, order_id=order_id
+ )
+ )
##
# Prepare data for the upcoming payment check.
- pay_params = dict(instance=INSTANCE,
- order_id=order_id,
- session_id=session_id)
+ pay_params = dict(
+ instance=INSTANCE, order_id=order_id, session_id=session_id
+ )
pay_status = backend_get("check-payment", pay_params)
@@ -311,13 +349,18 @@ def article(article_name, data=None):
# Somehow, a session with a payed article which _differs_ from
# the article requested in the URL existed; trigger the pay protocol!
if pay_status["contract_terms"]["extra"]["article_name"] != article_name:
- err_abort(402, message="You did not pay for this article (nice try!)", json=pay_status)
-
+ err_abort(
+ 402,
+ message="You did not pay for this article (nice try!)",
+ json=pay_status
+ )
+
##
# Show a "article refunded" page, in that case.
if pay_status.get("refunded"):
- return flask.render_template("templates/article_refunded.html",
- article_name=article_name)
+ return flask.render_template(
+ "templates/article_refunded.html", article_name=article_name
+ )
##
# Put the article in the cache.
paid_articles_cache.set(session_id + "-" + article_name, order_id)
@@ -327,10 +370,12 @@ def article(article_name, data=None):
return render_article(article_name, data, order_id)
elif pay_status.get("already_paid_order_id") is not None:
return flask.redirect(
- flask.url_for(
- "article",
- article_name=article_name,
- order_id=pay_status.get("already_paid_order_id")))
+ flask.url_for(
+ "article",
+ article_name=article_name,
+ order_id=pay_status.get("already_paid_order_id")
+ )
+ )
else:
##
# Redirect the browser to a page where the wallet can
@@ -338,16 +383,18 @@ def article(article_name, data=None):
taler_pay_uri = pay_status["taler_pay_uri"]
qrcode_svg = get_qrcode_svg(taler_pay_uri)
check_status_url_enc = urllib.parse.quote(
- flask.url_for(
- "check_status",
- order_id=order_id,
- session_id=session_id))
- content = flask.render_template("templates/request_payment.html",
- article_name=article_name,
- taler_pay_uri=taler_pay_uri,
- qrcode_svg=qrcode_svg,
- check_status_url_enc=check_status_url_enc)
- headers = { "Taler": taler_pay_uri }
+ flask.url_for(
+ "check_status", order_id=order_id, session_id=session_id
+ )
+ )
+ content = flask.render_template(
+ "templates/request_payment.html",
+ article_name=article_name,
+ taler_pay_uri=taler_pay_uri,
+ qrcode_svg=qrcode_svg,
+ check_status_url_enc=check_status_url_enc
+ )
+ headers = {"Taler": taler_pay_uri}
resp = flask.Response(content, status=402, headers=headers)
return resp
diff --git a/talerblog/blog/content.py b/talerblog/blog/content.py
index 8dddd1f..0ecfa66 100644
--- a/talerblog/blog/content.py
+++ b/talerblog/blog/content.py
@@ -33,6 +33,7 @@ Article = namedtuple("Article", "slug title teaser main_file extra_files")
# be made available in the blog.
ARTICLES = OrderedDict()
+
##
# Add article to the list of the available articles.
#
@@ -55,6 +56,7 @@ def get_image_file(image):
filex = resource_filename("talerblog", os.path.join("blog/data/", image))
return os.path.abspath(filex)
+
##
# Build the file path of a article.
#
@@ -81,7 +83,7 @@ def add_from_html(resource_name, teaser_paragraph=0, title=None):
soup = BeautifulSoup(res, 'html.parser')
res.close()
if title is None:
- title_el = soup.find("h1", attrs={"class":["chapter", "unnumbered"]})
+ title_el = soup.find("h1", attrs={"class": ["chapter", "unnumbered"]})
if title_el is None:
LOGGER.warning("Can't extract title from '%s'", resource_name)
title = resource_name
@@ -90,7 +92,7 @@ def add_from_html(resource_name, teaser_paragraph=0, title=None):
slug = title.replace(" ", "_")
paragraphs = soup.find_all("p")
- teaser = soup.find("p", attrs={"id":["teaser"]})
+ teaser = soup.find("p", attrs={"id": ["teaser"]})
if teaser is None:
teaser = paragraphs[teaser_paragraph].get_text()
else:
@@ -104,7 +106,10 @@ def add_from_html(resource_name, teaser_paragraph=0, title=None):
# component actually matches the article's slug
if re_proc.match(img['src']):
if img['src'].split(os.sep)[2] == slug:
- LOGGER.info("extra file for %s is %s" % (slug, os.path.basename(img['src'])))
+ LOGGER.info(
+ "extra file for %s is %s" %
+ (slug, os.path.basename(img['src']))
+ )
extra_files.append(os.path.basename(img['src']))
else:
LOGGER.warning("Image src and slug don't match: '%s' != '%s'" \
diff --git a/talerblog/talerconfig.py b/talerblog/talerconfig.py
index 4a44c97..1a33294 100644
--- a/talerblog/talerconfig.py
+++ b/talerblog/talerconfig.py
@@ -38,17 +38,20 @@ 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
+
##
# Do shell-style parameter expansion.
# Supported syntax:
@@ -80,7 +83,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)
@@ -93,9 +96,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]
@@ -104,6 +107,7 @@ def expand(var: str, getter: Callable[[str], str]) -> str:
return result + var[pos:]
+
##
# A configuration entry.
class Entry:
@@ -164,11 +168,16 @@ class Entry:
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
@@ -190,6 +199,7 @@ class Entry:
except ValueError:
raise ConfigurationError("Expected number for option '%s' in section '%s'" \
% (self.option.upper(), self.section.upper()))
+
##
# Fetch value to substitute to expansion variables.
#
@@ -231,6 +241,7 @@ class Entry:
return "<unknown>"
return "%s:%s" % (self.filename, self.lineno)
+
##
# Represent a section by inheriting from 'defaultdict'.
class OptionDict(collections.defaultdict):
@@ -280,6 +291,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):
@@ -313,6 +325,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.
@@ -323,7 +336,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.
@@ -362,7 +375,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.
@@ -377,7 +391,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.
@@ -391,7 +406,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.
@@ -465,36 +481,59 @@ 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:
# not logging here, as this interests the final user mostly.
@@ -503,23 +542,22 @@ class TalerConfig:
##
# 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,))
+ 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()))
-
##
# Return a whole section from this object.
#
@@ -538,14 +576,22 @@ 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/talerblog/tests.py b/talerblog/tests.py
index a5d1e8e..5de63db 100644
--- a/talerblog/tests.py
+++ b/talerblog/tests.py
@@ -37,8 +37,6 @@ class BlogTestCase(unittest.TestCase):
self.app = blog.app.test_client()
self.instance = TC["blog"]["instance"].value_string(required=True)
-
-
##
# Test the refund logic.
#
@@ -60,7 +58,11 @@ class BlogTestCase(unittest.TestCase):
response = self.app.get("/refund?order_id=99")
mocked_get.assert_called_with(
"http://backend.test.taler.net/refund",
- params={"order_id": "99", "instance": self.instance})
+ params={
+ "order_id": "99",
+ "instance": self.instance
+ }
+ )
# Test POST
mocked_session.get.return_value = {"mocckky": 99}
@@ -75,9 +77,12 @@ class BlogTestCase(unittest.TestCase):
"refund": {
"value": 0,
"fraction": 50000000,
- "currency": CURRENCY},
+ "currency": CURRENCY
+ },
"reason": "Demo reimbursement",
- "instance": self.instance})
+ "instance": self.instance
+ }
+ )
if __name__ == "__main__":