From 948f8b2af6d092862bec93ba3c987654db4f799e Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 29 Aug 2019 18:57:31 +0200 Subject: make pretty --- talerblog/blog/blog.py | 165 +++++++++++++++++++++++++++++----------------- talerblog/blog/content.py | 11 +++- talerblog/talerconfig.py | 112 +++++++++++++++++++++---------- talerblog/tests.py | 15 +++-- 4 files changed, 203 insertions(+), 100 deletions(-) (limited to '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//") 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 "" 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__": -- cgit v1.2.3