]> ToastFreeware Gitweb - philipp/winterrodeln/wrpylib.git/blobdiff - wrpylib/wrvalidators.py
Parse cachet.
[philipp/winterrodeln/wrpylib.git] / wrpylib / wrvalidators.py
index d3a11a694378f87f09aab49c47da58946ee9aa3e..3b0ae5f84156d130f0ba4aa916a44ebf85e7c9c4 100644 (file)
-#!/usr/bin/python3.4
-# -*- coding: iso-8859-15 -*-
-# $Id$
-# $HeadURL$
 """
-A converter is a Python variable (may be a class, class instance or anything else) that has the member
-functions from_str and to_str. From string takes a string "from the outside", checks it and returns a Python variable
-representing that value in Python. It reports error by raising ValueError. to_str does the opposite, however, it
-can assume that the value it has to convert to a string is valid. If it gets an invalid value, the behavior is
-undefined.
+This module contains functions that convert winterrodeln specific strings (like geographic coordinates) from string
+to appropriate python types and the other way round.
+Functions that take strings to convert it to python types are called *_from_str(value, [...]) and are supposed to
+validate the string. In case of errors, a ValueError (or a subclass thereof) is returned.
+Functions that take python types and convert it to Winterrodeln strings are called *_to_str(value, [...]) and can
+assume that the value they get is valid. If it is not, the behavior is undefined.
+The namedtuple FromToConverter groups corresponding *_from_str and *_to_str converters.
 """
-import datetime
 import email.headerregistry
 import urllib.parse
 import re
-import xml.dom.minidom as minidom
-from xml.parsers.expat import ExpatError
 from collections import OrderedDict, namedtuple
+from email.errors import HeaderParseError
+from typing import Tuple, Optional, List, Callable, Union, TypeVar, Dict, NamedTuple
 
-import mwparserfromhell
-import formencode
-import formencode.national
-from wrpylib.mwmarkup import template_to_table
+import mwparserfromhell  # https://github.com/earwig/mwparserfromhell
+from mwparserfromhell.nodes import Template
 
+from wrpylib.mwmarkup import format_template_table
 
-# Meta converter types and functions
-# ----------------------------------
 
+T = TypeVar("T")  # use for generic type annotations
+E = TypeVar("E")  # use for generic type annotations
+N = TypeVar("N")  # use for generic type annotations
+
+
+# FromToConverter type
+# --------------------
+
+# namedtuple that groups corresponding *_from_str and *_to_str functions.
 FromToConverter = namedtuple('FromToConverter', ['from_str', 'to_str'])
 
 
-def opt_from_str(value, from_str, none=None):
-    return none if value == '' else from_str(value)
+# optional converter
+# ------------------
+
+def opt_from_str(value: str, from_str: Callable[[str], T], empty: E = None) -> Union[T, E]:
+    """Makes the converter `from_str` "optional"
+    by replacing the empty string with a predefined value (default: None)."""
+    return empty if value == '' else from_str(value)
+
+
+def opt_to_str(value: Union[T, E], to_str: Callable[[T], str], empty: E = None) -> str:
+    return '' if value == empty else to_str(value)
+
+
+# "no" converter
+# --------------
+
+def no_german_from_str(value: str, from_str: Callable[[str], T], use_tuple: bool = True, no_value: N = None) \
+        -> Union[Tuple[bool, Union[T, N]], T, N]:
+    """Makes it possible to have "Nein" as special value. If use_tuple is True, a tuple is returned. The first
+    entry of the tuple is False in case the value is "Nein", otherwise the first value is True. The second value is
+    no_value in case of the value being "Nein", otherwise it is the result of from_str(value).
+    If use_tuple is False, no_value is returned in case the value is "Nein", otherwise the result of from_str(value)."""
+    if value == 'Nein':
+        return (False, no_value) if use_tuple else no_value
+    return (True, from_str(value)) if use_tuple else from_str(value)
 
 
-def opt_to_str(value, to_str, none=None):
-    return '' if value == none else to_str(value)
+def no_german_to_str(value: Union[Tuple[bool, Union[T, N]], T, N], to_str: Callable[[T], str], use_tuple: bool = True,
+                     no_value: N = None) -> str:
+    if use_tuple:
+        if not value[0]:
+            return 'Nein'
+        return to_str(value[1])
+    else:
+        if value == no_value:
+            return 'Nein'
+        return to_str(value)
 
 
+# "optional"/"no" converter
+# -------------------------
+
+def opt_no_german_from_str(value: str, from_str: Callable[[str], T], use_tuple: bool = True, no_value: N = None,
+                           empty: E = (None, None)) -> Union[Tuple[bool, Union[T, N]], T, N, E]:
+    """
+    'abc' -> (True, from_str('abc')) or from_str('abc')
+    'Nein' -> (False, no_value) or no_value
+    '' -> empty
+    """
+    return opt_from_str(value, lambda v: no_german_from_str(v, from_str, use_tuple, no_value), empty)
+
+
+def opt_no_german_to_str(value: Union[Tuple[bool, Union[T, N]], T, N, E], to_str: Callable[[T], str], use_tuple=True,
+                         no_value: N = None, empty: E = (None, None)) -> str:
+    """
+    (True, 'abc') -> to_value('abc')
+    (False, no_value) -> 'Nein'
+    empty -> ''
+    """
+    return opt_to_str(value, lambda v: no_german_to_str(v, to_str, use_tuple, no_value), empty)
+
+
+# choice converter
+# ----------------
+
 def choice_from_str(value, choices):
+    """Returns the value if it is a member of the choices iterable."""
     if value not in choices:
-        raise ValueError('{} is an invalid value')
+        raise ValueError(f"'{value}' is an invalid value")
     return value
 
 
-def dictkey_from_str(value, key_str_dict):
+# dictkey converter
+# -----------------
+
+def dictkey_from_str(value: str, key_str_dict: Dict[T, str]) -> T:
+    """Returns the key of an entry in the key_str_dict if the value of the entry corresponds to the given value."""
     try:
         return dict(list(zip(key_str_dict.values(), key_str_dict.keys())))[value]
     except KeyError:
-        raise ValueError("Invalid value '{}'".format(value))
+        raise ValueError(f"Invalid value '{value}'")
 
 
-def dictkey_to_str(value, key_str_dict):
+def dictkey_to_str(value: T, key_str_dict: Dict[T, str]) -> str:
     try:
         return key_str_dict[value]
     except KeyError:
-        raise ValueError("Invalid value '{}'".format(value))
+        raise ValueError(f"Invalid value '{value}'")
+
+
+# enum/"list" converter
+# ---------------------
+
+def enum_from_str(value: str, from_str: Callable[[str], T], separator: str = ';', min_len: int = 0) -> List[T]:
+    """Semicolon separated list of entries with the same "type"."""
+    values = value.split(separator)
+    if len(values) == 1 and values[0] == '':
+        values = []
+    if len(values) < min_len:
+        raise ValueError(f'at least {min_len} entry/entries have to be in the enumeration')
+    return list(map(from_str, map(str.strip, values)))
+
+
+def enum_to_str(value: List[T], to_str: Callable[[T], str], separator='; ') -> str:
+    return separator.join(map(to_str, value))
+
+
+# value/comment converter
+# -----------------------
+
+def value_comment_from_str(value: str, value_from_str: Callable[[str], T], comment_from_str: Callable[[str], E],
+                           comment_optional: bool = False) -> Tuple[T, E]:
+    """Makes it possible to have a mandatory comment in parenthesis at the end of the string."""
+    comment = ''
+    if value.endswith(')'):
+        open_brackets = 0
+        for i, char in enumerate(value[::-1]):
+            if char == ')':
+                open_brackets += 1
+            elif char == '(':
+                open_brackets -= 1
+                if open_brackets == 0:
+                    comment = value[-i:-1]
+                    value = value[:-i-1]
+                    if len(value) > 1 and value[-1] != ' ':
+                        raise ValueError('there has to be a space before the opening bracket of the comment')
+                    value = value[:-1]
+                    break
+        else:
+            if open_brackets > 0:
+                raise ValueError('bracket mismatch')
+            if not comment_optional:
+                raise ValueError('mandatory comment not found')
+    else:
+        if not comment_optional:
+            raise ValueError(f'mandatory comment not found in "{value}"')
+    return value_from_str(value), comment_from_str(comment)
+
 
+def value_comment_to_str(value: Tuple[T, E], value_to_str: Callable[[T], str], comment_to_str: Callable[[E], str],
+                         comment_optional: bool = False) -> str:
+    left = value_to_str(value[0])
+    comment = comment_to_str(value[1])
+    if len(comment) > 0 or not comment_optional:
+        comment = f'({comment})'
+    if len(left) == 0:
+        return comment
+    if len(comment) == 0:
+        return left
+    return f'{left} {comment}'
 
-# Basic type converter functions
-# ------------------------------
 
+# string converter
+# ----------------
 
-def str_from_str(value):
+def str_from_str(value: str) -> str:
+    """Converter that takes any string and returns it as string without validation.
+    In other words, this function does nothing and just returns its argument."""
     return value
 
 
-def str_to_str(value):
+def str_to_str(value: str) -> str:
     return value
 
 
-def opt_str_from_str(value):
+def req_str_from_str(value: str) -> str:
+    if value == '':
+        raise ValueError('missing required value')
+    return str_from_str(value)
+
+
+def opt_str_from_str(value: str) -> Optional[str]:
     return opt_from_str(value, str_from_str)
 
 
-def opt_str_to_str(value):
+def opt_str_to_str(value: Optional[str]) -> str:
     return opt_to_str(value, str_to_str)
 
 
 opt_str_converter = FromToConverter(opt_str_from_str, opt_str_to_str)
 
 
-def req_str_from_str(value):
-    if value == '':
-        raise ValueError('missing required value')
-    return str_from_str(value)
+# optional no or string converter
+# -------------------------------
+
+def opt_no_or_str_from_str(value: str) -> Tuple[Optional[bool], Optional[str]]:
+    """
+    'Nein' => (False, None); 'Nur Wochenende' => (True, 'Nur Wochenende'); 'Ja' => (True, 'Ja'); '' => (None, None)"""
+    return opt_no_german_from_str(value, req_str_from_str)
 
 
-def int_from_str(value, min=None, max=None):
+def opt_no_or_str_to_str(value: Tuple[Optional[bool], Optional[str]]) -> str:
+    return opt_no_german_to_str(value, str_to_str)
+
+
+opt_no_or_str_converter = FromToConverter(opt_no_or_str_from_str, opt_no_or_str_to_str)
+
+
+# integer converter
+# -----------------
+
+def int_from_str(value: str, minimum: Optional[int] = None, maximum: Optional[int] = None) -> int:
+    """Converter that takes a string representation of an integer and returns the integer.
+    :param value: string representation of an integer
+    :param minimum: If not None, the integer has to be at least min
+    :param maximum: If not None, the integer has to be no more than max
+    """
     value = int(value)
-    if min is not None and value < min:
-        raise ValueError('{} must be >= than {}'.format(value, min))
-    if max is not None and value > max:
-        raise ValueError('{} must be <= than {}'.format(value, max))
+    if minimum is not None and value < minimum:
+        raise ValueError(f'{value} must be >= than {minimum}')
+    if maximum is not None and value > maximum:
+        raise ValueError(f'{value} must be <= than {maximum}')
     return value
 
 
-def int_to_str(value):
+def int_to_str(value: int) -> str:
     return str(value)
 
 
-def opt_int_from_str(value, min=None, max=None):
-    return opt_from_str(value, lambda val: int_from_str(val, min, max))
+def opt_int_from_str(value: str, minimum: Optional[int] = None, maximum: Optional[int] = None) -> Optional[int]:
+    return opt_from_str(value, lambda val: int_from_str(val, minimum, maximum))
 
 
-def opt_int_to_str(value):
+def opt_int_to_str(value: Optional[int]) -> str:
     return opt_to_str(value, int_to_str)
 
 
-opt_int_converter = FromToConverter(opt_int_from_str, opt_int_to_str)
+def opt_uint_from_str(value: str, minimum: int = 0, maximum: Optional[int] = None) -> Optional[int]:
+    """Optional positive integer."""
+    return opt_int_from_str(value, minimum, maximum)
 
 
-IntConverter = FromToConverter(int_from_str, int_to_str)
+def opt_uint_to_str(value: Optional[int]) -> str:
+    return opt_int_to_str(value)
 
 
-# Complex types
-# -------------
-
-def enum_from_str(value, from_str=req_str_from_str, separator=';', min_len=0):
-    """Semicolon separated list of entries with the same "type"."""
-    values = value.split(separator)
-    if len(values) == 1 and values[0] == '':
-        values = []
-    if len(values) < min_len:
-        raise ValueError('at least {} entry/entries have to be in the enumeration'.format(min_len))
-    return list(map(from_str, map(str.strip, values)))
+opt_uint_converter = FromToConverter(opt_uint_from_str, opt_uint_to_str)
 
 
-def enum_to_str(value, to_str=opt_str_to_str, separator='; '):
-    return separator.join(map(to_str, value))
-
-
-# Specific converter functions
-# ----------------------------
+# bool converter
+# --------------
 
 BOOL_GERMAN = OrderedDict([(False, 'Nein'), (True, 'Ja')])
 
 
-def bool_german_from_str(value):
+def bool_german_from_str(value: str) -> bool:
     return dictkey_from_str(value, BOOL_GERMAN)
 
 
-def bool_german_to_str(value):
+def bool_german_to_str(value: bool) -> str:
     return dictkey_to_str(value, BOOL_GERMAN)
 
 
-def opt_bool_german_from_str(value):
+def opt_bool_german_from_str(value: str) -> Optional[bool]:
     return opt_from_str(value, bool_german_from_str)
 
 
-def opt_bool_german_to_str(value):
+def opt_bool_german_to_str(value: Optional[bool]) -> str:
     return opt_to_str(value, bool_german_to_str)
 
 
 opt_bool_german_converter = FromToConverter(opt_bool_german_from_str, opt_bool_german_to_str)
 
 
+# tristate converter
+# ------------------
+
 TRISTATE_GERMAN = OrderedDict([(0.0, 'Nein'), (0.5, 'Teilweise'), (1.0, 'Ja')])
 
 
-def tristate_german_from_str(value):
+def tristate_german_from_str(value: str) -> float:
     return dictkey_from_str(value, TRISTATE_GERMAN)
 
 
-def tristate_german_to_str(value):
+def tristate_german_to_str(value: float) -> str:
     return dictkey_to_str(value, TRISTATE_GERMAN)
 
 
-def opt_tristate_german_from_str(value):
+def opt_tristate_german_from_str(value: str) -> Optional[float]:
     return opt_from_str(value, tristate_german_from_str)
 
 
-def opt_tristate_german_to_str(value):
+def opt_tristate_german_to_str(value: Optional[float]) -> str:
     return opt_to_str(value, tristate_german_to_str)
 
 
-def meter_from_str(value):
-    return int_from_str(value, min=0)
-
-
-def meter_to_str(value):
-    return int_to_str(value)
-
-
-def opt_meter_from_str(value):
-    return opt_from_str(value, meter_from_str)
-
-
-def opt_meter_to_str(value):
-    return opt_to_str(value, meter_to_str)
-
-
-opt_meter_converter = FromToConverter(opt_meter_from_str, opt_meter_to_str)
-
-
-def minutes_from_str(value):
-    return int_from_str(value, min=0)
+opt_tristate_german_converter = FromToConverter(opt_tristate_german_from_str, opt_tristate_german_to_str)
 
 
-def minutes_to_str(value):
-    return int_to_str(value)
+# tristate with comment converter
+# -------------------------------
 
-
-def opt_minutes_from_str(value):
-    return opt_from_str(value, minutes_from_str)
-
-
-def opt_minutes_to_str(value):
-    return opt_to_str(value, minutes_to_str)
-
-
-opt_minutes_converter = FromToConverter(opt_minutes_from_str, opt_minutes_to_str)
-
-
-LonLat = namedtuple('LonLat', ['lon', 'lat'])
+def opt_tristate_german_comment_from_str(value: str) -> Tuple[Optional[float], Optional[str]]:
+    """Ja, Nein or Teilweise, optionally with comment in parenthesis."""
+    return value_comment_from_str(value, opt_tristate_german_from_str, opt_str_from_str, True)
 
 
-lonlat_none = LonLat(None, None)
+def opt_tristate_german_comment_to_str(value: Tuple[Optional[float], Optional[str]]) -> str:
+    return value_comment_to_str(value, opt_tristate_german_to_str, opt_str_to_str, True)
 
 
-def lonlat_from_str(value):
-    """Converts a winterrodeln geo string like '47.076207 N 11.453553 E' (being '<latitude> N <longitude> E'
-    to the LonLat(lon, lat) named  tupel."""
-    r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value)
-    if r is None: raise ValueError("Coordinates '{}' have not a format like '47.076207 N 11.453553 E'".format(value))
-    return LonLat(float(r.groups()[1]), float(r.groups()[0]))
+opt_tristate_german_comment_converter = FromToConverter(opt_tristate_german_comment_from_str,
+                                                        opt_tristate_german_comment_to_str)
 
 
-def lonlat_to_str(value):
-    return '{:.6f} N {:.6f} E'.format(value.lat, value.lon)
+# url converter
+# -------------
 
+def url_from_str(value: str) -> str:
+    result = urllib.parse.urlparse(value)
+    if result.scheme not in ['http', 'https']:
+        raise ValueError('scheme has to be http or https')
+    if not result.netloc:
+        raise ValueError('url does not contain netloc')
+    return value
 
-def opt_lonlat_from_str(value):
-    return opt_from_str(value, lonlat_from_str, lonlat_none)
 
+def url_to_str(value: str) -> str:
+    return value
 
-def opt_lonlat_to_str(value):
-    return opt_to_str(value, lonlat_to_str, lonlat_none)
 
+# webauskunft converter
+# ---------------------
 
-opt_lonlat_converter = FromToConverter(opt_lonlat_from_str, opt_lonlat_to_str)
+def webauskunft_from_str(value: str) -> Tuple[Optional[bool], Optional[str]]:
+    """Converts a URL or 'Nein' to a tuple
+    'http://www.example.com/' -> (True, 'http://www.example.com/')
+    'Nein' -> (False, None)
+    '' -> (None, None)
 
+    :param value: URL or 'Nein'
+    :return: tuple
+    """
+    return opt_no_german_from_str(value, url_from_str)
 
 
-class MultiGeo(formencode.FancyValidator):
-    "Formats multiple coordinates, even in multiple lines to [(latitude, longitude, elevation), ...] or [(latitude, longitude, None), ...] tuplets."
-    
-    # Valid for input_format
-    FORMAT_GUESS = 0         # guesses the input format; default for input_format
-    FORMAT_NONE = -1          # indicates missing formats
-    
-    # Valid for input_format and output_format
-    FORMAT_GEOCACHING = 1    # e.g. "N 47° 13.692 E 011° 25.535"
-    FORMAT_WINTERRODELN = 2  # e.g. "47.222134 N 11.467211 E"
-    FORMAT_GMAPPLUGIN = 3    # e.g. "47.232922, 11.452239"
-    FORMAT_GPX = 4           # e.g. "<trkpt lat="47.181289" lon="11.408827"><ele>1090.57</ele></trkpt>"
-    
-    input_format = FORMAT_GUESS
-    output_format = FORMAT_WINTERRODELN
-    last_input_format = FORMAT_NONE
-
-    def __init__(self, input_format = FORMAT_GUESS, output_format = FORMAT_WINTERRODELN, **keywords):
-        self.input_format = input_format
-        self.output_format = output_format
-        formencode.FancyValidator.__init__(self, if_empty = (None, None, None), **keywords)
-    
-    def to_python(self, value, state=None):
-        self.assert_string(value, state)
-        input_format = self.input_format
-        if not input_format in [self.FORMAT_GUESS, self.FORMAT_GEOCACHING, self.FORMAT_WINTERRODELN, self.FORMAT_GMAPPLUGIN, self.FORMAT_GPX]:
-            raise formencode.Invalid("input_format %d is not recognized" % input_format, value, state) # Shouldn't it be an other type of runtime error?
-        lines = [line.strip() for line in value.split("\n") if len(line.strip()) > 0]
-        
-        result = []
-        for line in lines:
-            if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GEOCACHING:
-                r = re.match('N ?(\d+)° ?(\d+\.\d+) +E ?(\d+)° ?(\d+\.\d+)', line)
-                if not r is None:
-                    g = r.groups()
-                    result.append((float(g[0]) + float(g[1])/60, float(g[2]) + float(g[3])/60, None))
-                    last_input_format = self.FORMAT_WINTERRODELN
-                    continue
-                    
-            if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_WINTERRODELN:
-                r = re.match('(\d+\.\d+) N (\d+\.\d+) E', line)
-                if not r is None:
-                    result.append((float(r.groups()[0]), float(r.groups()[1]), None))
-                    last_input_format = self.FORMAT_WINTERRODELN
-                    continue
-                
-            if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GMAPPLUGIN:
-                r = re.match('(\d+\.\d+), ?(\d+\.\d+)', line)
-                if not r is None:
-                    result.append((float(r.groups()[0]), float(r.groups()[1]), None))
-                    last_input_format = self.FORMAT_GMAPPLUGIN
-                    continue
-                
-            if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GPX:
-                try:
-                    xml = minidom.parseString(line)
-                    coord = xml.documentElement
-                    lat = float(coord.getAttribute('lat'))
-                    lon = float(coord.getAttribute('lon'))
-                    try: ele = float(coord.childNodes[0].childNodes[0].nodeValue)
-                    except (IndexError, ValueError): ele = None
-                    result.append((lat, lon, ele))
-                    last_input_format = self.FORMAT_GPX
-                    continue
-                except (ExpatError, IndexError, ValueError): pass
-
-            raise formencode.Invalid("Coordinates '%s' have no known format" % line, value, state)
-            
-        return result
-    
-    def from_python(self, value, state=None):
-        output_format = self.output_format
-        result = []
-        for latitude, longitude, height in value:
-            if output_format == self.FORMAT_GEOCACHING:
-                degree = latitude
-                result.append('N %02d° %02.3f E %03d° %02.3f' % (latitude, latitude % 1 * 60, longitude, longitude % 1 * 60))
-                
-            elif output_format == self.FORMAT_WINTERRODELN:
-                result.append('%.6f N %.6f E' % (latitude, longitude))
-
-            elif output_format == self.FORMAT_GMAPPLUGIN:
-                result.append('%.6f, %.6f' % (latitude, longitude))
-                
-            elif output_format == self.FORMAT_GPX:
-                if not height is None: result.append('<trkpt lat="%.6f" lon="%.6f"><ele>%.2f</ele></trkpt>' % (latitude, longitude, height))
-                else: result.append('<trkpt lat="%.6f" lon="%.6f"/>' % (latitude, longitude))
-            
-            else:
-                raise formencode.Invalid("output_format %d is not recognized" % output_format, value, state) # Shouldn't it be an other type of runtime error?
-            
-        return "\n".join(result)
+def webauskunft_to_str(value: Tuple[Optional[bool], Optional[str]]) -> str:
+    return opt_no_german_to_str(value, url_to_str)
 
 
-DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')])
+webauskunft_converter = FromToConverter(webauskunft_from_str, webauskunft_to_str)
 
 
-def difficulty_german_from_str(value):
-    return dictkey_from_str(value, DIFFICULTY_GERMAN)
+# wikipage converter
+# ------------------
 
+def wikipage_from_str(value: str) -> str:
+    """Validates wiki page name like '[[Birgitzer Alm]]'.
+    The page is not checked for existence.
+    An empty string is an error.
+    '[[Birgitzer Alm]]' => '[[Birgitzer Alm]]'
+    """
+    if re.match(r'\[\[[^\[\]]+]]$', value) is None:
+        raise ValueError(f'No valid wiki page name "{value}"')
+    return value
 
-def difficulty_german_to_str(value):
-    return dictkey_to_str(value, DIFFICULTY_GERMAN)
 
+def wikipage_to_str(value: str) -> str:
+    return value
 
-def opt_difficulty_german_from_str(value):
-    return opt_from_str(value, difficulty_german_from_str)
 
+def opt_wikipage_enum_from_str(value: str) -> Optional[List[str]]:
+    """Validates a list of wiki pages like '[[Birgitzer Alm]]; [[Kemater Alm]]'.
+    '[[Birgitzer Alm]]; [[Kemater Alm]]' => ['[[Birgitzer Alm]]', '[[Kemater Alm]]']
+    '[[Birgitzer Alm]]'                  => ['[[Birgitzer Alm]]']
+    'Nein'                               => []
+    ''                                   => None
+    """
+    return opt_no_german_from_str(value, lambda val: enum_from_str(val, wikipage_from_str), False, [], None)
 
-def opt_difficulty_german_to_str(value):
-    return opt_to_str(value, difficulty_german_to_str)
 
+def opt_wikipage_enum_to_str(value: Optional[List[str]]) -> str:
+    return opt_no_german_to_str(value, lambda val: enum_to_str(val, wikipage_to_str), False, [], None)
 
-opt_difficulty_german_converter = FromToConverter(opt_difficulty_german_from_str, opt_difficulty_german_to_str)
 
+opt_wikipage_enum_converter = FromToConverter(opt_wikipage_enum_from_str, opt_wikipage_enum_to_str)
 
-AVALANCHES_GERMAN = OrderedDict([(1, 'kaum'), (2, 'selten'), (3, 'gelegentlich'), (4, 'häufig')])
 
+# email converter
+# ---------------
 
-def avalanches_german_from_str(value):
-    return dictkey_from_str(value, AVALANCHES_GERMAN)
+def email_from_str(value: str) -> str:
+    """Takes an email address like 'office@example.com', checks it for correctness and returns it again as string."""
+    try:
+        email.headerregistry.Address(addr_spec=value)
+    except HeaderParseError as e:
+        raise ValueError(f'Invalid email address: {value}', e)
+    return value
 
 
-def avalanches_german_to_str(value):
-    return dictkey_to_str(value, AVALANCHES_GERMAN)
+def email_to_str(value: str) -> str:
+    return str(value)
 
 
-def opt_avalanches_german_from_str(value):
-    return opt_from_str(value, avalanches_german_from_str)
+def masked_email_from_str(value: str, mask='(at)', masked_only=False) -> Tuple[str, bool]:
+    """Converts an email address that is possibly masked. Returns a tuple. The first parameter is the un-masked
+    email address as string, the second is a boolean telling whether the address was masked."""
+    unmasked = value.replace(mask, '@')
+    was_masked = unmasked != value
+    if masked_only and not was_masked:
+        raise ValueError('E-Mail address not masked')
+    return email_from_str(unmasked), was_masked
 
 
-def opt_avalanches_german_to_str(value):
-    return opt_to_str(value, avalanches_german_to_str)
+def masked_email_to_str(value: Tuple[str, bool], mask='(at)') -> str:
+    """Value is a tuple. The first entry is the email address, the second one is a boolean telling whether the
+    email address should be masked."""
+    email_, do_masking = value
+    email_ = email_to_str(email_)
+    if do_masking:
+        email_ = email_.replace('@', mask)
+    return email_
 
 
-opt_avalanches_german_converter = FromToConverter(opt_avalanches_german_from_str, opt_avalanches_german_to_str)
+def emails_from_str(value: str) -> Optional[List[Tuple[str, str]]]:
+    return opt_no_german_from_str(
+        value,
+        lambda val:
+            enum_from_str(val, lambda v: value_comment_from_str(v, masked_email_from_str, opt_str_from_str, True)),
+        False, [], None)
 
 
-PUBLIC_TRANSPORT_GERMAN = OrderedDict([(1, 'Sehr gut'), (2, 'Gut'), (3, 'Mittelmäßig'), (4, 'Schlecht'), (5, 'Nein'), (6, 'Ja')])
+def emails_to_str(value: Optional[List[Tuple[str, str]]]) -> str:
+    return opt_no_german_to_str(
+        value,
+        lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, masked_email_to_str, opt_str_to_str, True)),
+        False, [], None)
 
 
-def public_transport_german_from_str(value):
-    return dictkey_from_str(value, PUBLIC_TRANSPORT_GERMAN)
+emails_converter = FromToConverter(emails_from_str, emails_to_str)
 
 
-def public_transport_german_to_str(value):
-    return dictkey_to_str(value, PUBLIC_TRANSPORT_GERMAN)
+# phone converter
+# ---------------
 
+def phone_number_from_str(value: str) -> str:
+    match = re.match(r'\+\d+(-\d+)*$', value)
+    if match is None:
+        raise ValueError('invalid format of phone number - use something like +43-699-1234567')
+    return value
 
-def opt_public_transport_german_from_str(value):
-    return opt_from_str(value, public_transport_german_from_str)
 
+def phone_number_to_str(value: str) -> str:
+    return value
 
-def opt_public_transport_german_to_str(value):
-    return opt_to_str(value, public_transport_german_to_str)
 
+def opt_phone_comment_enum_from_str(value: str, comment_optional: bool = False) -> Optional[List[Tuple[str, str]]]:
+    return opt_no_german_from_str(
+        value,
+        lambda val: enum_from_str(
+            val,
+            lambda v:
+                value_comment_from_str(
+                    v,
+                    phone_number_from_str,
+                    opt_str_from_str if comment_optional else req_str_from_str,
+                    comment_optional
+                )
+        ), False, [], None)
 
-opt_public_transport_german_converter = FromToConverter(opt_public_transport_german_from_str, opt_public_transport_german_to_str)
 
+def opt_phone_comment_enum_to_str(value: Optional[List[Tuple[str, str]]], comment_optional: bool = False) -> str:
+    return opt_no_german_to_str(
+        value,
+        lambda val: enum_to_str(
+            val, lambda v: value_comment_to_str(
+                v, phone_number_to_str, opt_str_to_str if comment_optional else str_to_str, comment_optional)),
+        False, [], None)
 
-def value_comment_from_str(value, value_from_str=str_from_str, comment_from_str=str_from_str, comment_optional=False):
-    """Makes it possible to have a mandatory comment in parenthesis at the end of the string."""
-    open_brackets = 0
-    comment = ''
-    comment_end_pos = None
-    for i, char in enumerate(value[::-1]):
-        if char == ')':
-            open_brackets += 1
-            if open_brackets == 1:
-                comment_end_pos = i
-                if len(value[-1-comment_end_pos:].rstrip()) > 1:
-                    raise ValueError('invalid characters after comment')
-        elif char == '(':
-            open_brackets -= 1
-            if open_brackets == 0:
-                comment = value[-i:-1-comment_end_pos]
-                value = value[:-i-1].rstrip()
-                break
-    else:
-        if open_brackets > 0:
-            raise ValueError('bracket mismatch')
-        if not comment_optional:
-            raise ValueError('mandatory comment not found')
-    return value_from_str(value), comment_from_str(comment)
 
+opt_phone_comment_enum_converter = FromToConverter(opt_phone_comment_enum_from_str, opt_phone_comment_enum_to_str)
 
-def value_comment_to_str(value, value_to_str=str_to_str, comment_to_str=str_to_str, comment_optional=False):
-    left = value_to_str(value[0])
-    comment = comment_to_str(value[1])
-    if len(comment) > 0 or not comment_optional:
-        comment = '({})'.format(comment)
-    if len(left) == 0:
-        return comment
-    if len(comment) == 0:
-        return left
-    return '{} {}'.format(left, comment)
 
+opt_phone_comment_opt_enum_converter = FromToConverter(lambda value: opt_phone_comment_enum_from_str(value, True),
+                                                       lambda value: opt_phone_comment_enum_to_str(value, True))
 
-def opt_tristate_german_comment_from_str(value):
-    """Ja, Nein or Vielleicht, optionally with comment in parenthesis."""
-    return value_comment_from_str(value, opt_tristate_german_from_str, opt_str_from_str, True)
 
+# longitude/latitude converter
+# ----------------------------
 
-def opt_tristate_german_comment_to_str(value):
-    return value_comment_to_str(value, opt_tristate_german_to_str, opt_str_to_str, True)
+class LonLat(NamedTuple):
+    lon: float
+    lat: float
 
 
-opt_tristate_german_comment_converter = FromToConverter(opt_tristate_german_comment_from_str, opt_tristate_german_comment_to_str)
+def lonlat_from_str(value: str) -> LonLat:
+    """Converts a Winterrodeln geo string like '47.076207 N 11.453553 E' (being '<latitude> N <longitude> E'
+    to the LonLat(lon, lat) named  tuple."""
+    r = re.match(r'(\d+\.\d+) N (\d+\.\d+) E', value)
+    if r is None:
+        raise ValueError(f"Coordinates '{value}' have not a format like '47.076207 N 11.453553 E'")
+    return LonLat(float(r.groups()[1]), float(r.groups()[0]))
 
 
-def no_german_from_str(value, from_str=req_str_from_str, use_tuple=True, no_value=None):
-    if value == 'Nein':
-        return (False, no_value) if use_tuple else no_value
-    return (True, from_str(value)) if use_tuple else from_str(value)
+def lonlat_to_str(value: LonLat) -> str:
+    return f'{value.lat:.6f} N {value.lon:.6f} E'
 
 
-def no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None):
-    if use_tuple:
-        if not value[0]:
-            return 'Nein'
-        return to_str(value[1])
-    else:
-        if value == no_value:
-            return 'Nein'
-        return to_str(value)
+def opt_lonlat_from_str(value: str) -> Optional[LonLat]:
+    return opt_from_str(value, lonlat_from_str, None)
 
 
-def opt_no_german_from_str(value, from_str=str_from_str, use_tuple=True, no_value=None, none=(None, None)):
-    return opt_from_str(value, lambda v: no_german_from_str(v, from_str, use_tuple, no_value), none)
+def opt_lonlat_to_str(value: Optional[LonLat]) -> str:
+    return opt_to_str(value, lonlat_to_str, None)
 
 
-def opt_no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None, none=(None, None)):
-    return opt_to_str(value, lambda v: no_german_to_str(v, to_str, use_tuple, no_value), none)
+opt_lonlat_converter = FromToConverter(opt_lonlat_from_str, opt_lonlat_to_str)
 
 
-def night_light_from_str(value):
-    """'Beleuchtungsanlage' Tristate with optional comment:
-    ''                  <=> (None, None)
-    'Ja'                <=> (1.0,  None)
-    'Teilweise'         <=> (0.5,  None)
-    'Nein'              <=> (0.0,  None)
-    'Ja (aber schmal)'  <=> (1.0,  'aber schmal')
-    'Teilweise (oben)'  <=> (0.5,  'oben')
-    'Nein (aber breit)' <=> (0.0,  'aber breit')
-    """
-    return
+# difficulty converter
+# --------------------
 
+DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')])
 
-def nightlightdays_from_str(value):
-    return value_comment_from_str(value, lambda val: opt_from_str(val, lambda v: int_from_str(v, min=0, max=7)), opt_str_from_str, comment_optional=True)
 
+def difficulty_german_from_str(value: str) -> int:
+    return dictkey_from_str(value, DIFFICULTY_GERMAN)
 
-def nightlightdays_to_str(value):
-    return value_comment_to_str(value, lambda val: opt_to_str(val, int_to_str), opt_str_to_str, comment_optional=True)
 
+def difficulty_german_to_str(value: int) -> str:
+    return dictkey_to_str(value, DIFFICULTY_GERMAN)
 
-nightlightdays_converter = FromToConverter(nightlightdays_from_str, nightlightdays_to_str)
 
+def opt_difficulty_german_from_str(value: str) -> Optional[int]:
+    return opt_from_str(value, difficulty_german_from_str)
 
-CACHET_REGEXP = [r'(Tiroler Naturrodelbahn-Gütesiegel) ([12]\d{3}) (leicht|mittel|schwer)$']
 
+def opt_difficulty_german_to_str(value: Optional[int]) -> str:
+    return opt_to_str(value, difficulty_german_to_str)
 
-def single_cachet_german_from_str(value):
-    for pattern in CACHET_REGEXP:
-        match = re.match(pattern, value)
-        if match:
-            return match.groups()
-    raise ValueError("'{}' is no valid cachet".format(value))
 
+opt_difficulty_german_converter = FromToConverter(opt_difficulty_german_from_str, opt_difficulty_german_to_str)
 
-def single_cachet_german_to_str(value):
-    return ' '.join(value)
 
+# avalanches converter
+# --------------------
 
-def cachet_german_from_str(value):
-    """Converts a "Gütesiegel":
-    '' => None
-    'Nein' => []
-    'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' => [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]"""
-    return opt_no_german_from_str(value, lambda val: enum_from_str(val, single_cachet_german_from_str), False, [], None)
+AVALANCHES_GERMAN = OrderedDict([(1, 'kaum'), (2, 'selten'), (3, 'gelegentlich'), (4, 'häufig')])
 
-    
-def cachet_german_to_str(value):
-    return opt_no_german_to_str(value, lambda val: enum_to_str(val, single_cachet_german_to_str), False, [], None)
 
+def avalanches_german_from_str(value: str) -> int:
+    return dictkey_from_str(value, AVALANCHES_GERMAN)
 
-cachet_german_converter = FromToConverter(cachet_german_from_str, cachet_german_to_str)
 
+def avalanches_german_to_str(value: int) -> str:
+    return dictkey_to_str(value, AVALANCHES_GERMAN)
 
-def url_from_str(value):
-    result = urllib.parse.urlparse(value)
-    if result.scheme not in ['http', 'https']:
-        raise ValueError('scheme has to be http or https')
-    if not result.netloc:
-        raise ValueError('url does not contain netloc')
-    return value
 
+def opt_avalanches_german_from_str(value: str) -> Optional[int]:
+    return opt_from_str(value, avalanches_german_from_str)
 
-def url_to_str(value):
-    return value
 
+def opt_avalanches_german_to_str(value: Optional[int]) -> str:
+    return opt_to_str(value, avalanches_german_to_str)
 
-def webauskunft_from_str(value):
-    return opt_no_german_from_str(value, url_from_str)
 
+opt_avalanches_german_converter = FromToConverter(opt_avalanches_german_from_str, opt_avalanches_german_to_str)
 
-def webauskunft_to_str(value):
-    return opt_no_german_to_str(value, url_to_str)
 
+# lift converter
+# --------------
 
-webauskunft_converter = FromToConverter(webauskunft_from_str, webauskunft_to_str)
+LIFT_GERMAN = ['Sessellift', 'Gondel', 'Linienbus', 'Taxi', 'Sonstige']
 
 
-class Url(formencode.FancyValidator):
-    """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed."""
-    # formencode 1.2.5 to formencode 1.3.0a1 sometimes raise ValueError instead of Invalid exceptions
-    # https://github.com/formencode/formencode/pull/61
-    urlv = formencode.validators.URL()    
+def lift_german_from_str(value) -> Optional[List[Tuple[str, Optional[str]]]]:
+    """Checks a lift_details property. It is a value comment property with the following
+    values allowed:
+    'Sessellift'
+    'Gondel'
+    'Linienbus'
+    'Taxi'
+    'Sonstige'
+    Alternatively, the value u'Nein' is allowed.
+    An empty string maps to (None, None).
 
-    def to_python(self, value, state=None):
-        self.assert_string(value, state)
-        v = value
-        v = v.replace('ä', 'a')
-        v = v.replace('ö', 'o')
-        v = v.replace('ü', 'u')
-        v = v.replace('ß', 'ss')
-        v = self.urlv.to_python(v, state)
-        return value
-    
-    def from_python(self, value, state=None):
-        return value
+    Examples:
+    ''                                       <=> None
+    'Nein'                                   <=> []
+    'Sessellift                              <=> [('Sessellift', None)]
+    'Gondel (nur bis zur Hälfte)'            <=> [('Gondel', 'nur bis zur Hälfte')]
+    'Sessellift; Taxi'                       <=> [('Sessellift', None), ('Taxi', None)]
+    'Sessellift (Wochenende); Taxi (6 Euro)' <=> [('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')]
+    """
+    return opt_no_german_from_str(
+        value,
+        lambda value_enum:
+            enum_from_str(
+                value_enum,
+                lambda value_comment:
+                    value_comment_from_str(
+                        value_comment,
+                        lambda v: choice_from_str(v, LIFT_GERMAN),
+                        opt_str_from_str,
+                        comment_optional=True)
+            ), use_tuple=False, no_value=[], empty=None)
+
+
+def lift_german_to_str(value: Optional[List[Tuple[str, Optional[str]]]]) -> str:
+    return opt_no_german_to_str(
+        value,
+        lambda value_enum:
+            enum_to_str(
+                value_enum,
+                lambda value_comment:
+                    value_comment_to_str(
+                        value_comment,
+                        str_to_str,
+                        opt_str_to_str,
+                        comment_optional=True)
+            ),
+        use_tuple=False, no_value=[], empty=None)
 
 
-def phone_number_from_str(value):
-    match = re.match(r'\+\d+(-\d+)*$', value)
-    if match is None:
-        raise ValueError('invalid format of phone number - use something like +43-699-1234567')
-    return value
+lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str)
 
 
-def phone_number_to_str(value):
-    return value
+# public transport converter
+# --------------------------
 
+PUBLIC_TRANSPORT_GERMAN = OrderedDict(
+    [(1, 'Sehr gut'), (2, 'Gut'), (3, 'Mittelmäßig'), (4, 'Schlecht'), (5, 'Nein'), (6, 'Ja')])
 
-def telefonauskunft_from_str(value):
-    return opt_no_german_from_str(value, lambda val: enum_from_str(val, lambda v: value_comment_from_str(v, phone_number_from_str, req_str_from_str, False)), False, [], None)
 
+def public_transport_german_from_str(value: str) -> int:
+    return dictkey_from_str(value, PUBLIC_TRANSPORT_GERMAN)
 
-def telefonauskunft_to_str(value):
-    return opt_no_german_to_str(value, lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, phone_number_to_str, str_to_str)), False, [], None)
 
+def public_transport_german_to_str(value: int) -> str:
+    return dictkey_to_str(value, PUBLIC_TRANSPORT_GERMAN)
 
-telefonauskunft_converter = FromToConverter(telefonauskunft_from_str, telefonauskunft_to_str)
 
+def opt_public_transport_german_from_str(value: str) -> Optional[int]:
+    return opt_from_str(value, public_transport_german_from_str)
 
-def email_from_str(value):
-    """Takes an email address like 'office@example.com', checks it for correctness and returns it again as string."""
-    try:
-        email.headerregistry.Address(addr_spec=value)
-    except email.errors.HeaderParseError as e:
-        raise ValueError('Invalid email address: {}'.format(value), e)
-    return value
 
+def opt_public_transport_german_to_str(value: Optional[int]) -> str:
+    return opt_to_str(value, public_transport_german_to_str)
 
-def email_to_str(value):
-    return str(value)
 
+opt_public_transport_german_converter = FromToConverter(opt_public_transport_german_from_str,
+                                                        opt_public_transport_german_to_str)
 
-def masked_email_from_str(value, mask='(at)', masked_only=False):
-    """Converts an email address that is possibly masked. Returns a tuple. The first parameter is the un-masked
-    email address as string, the second is a boolean telling whether the address was masked."""
-    unmasked = value.replace(mask, '@')
-    was_masked = unmasked != value
-    if masked_only and not was_masked:
-        raise ValueError('E-Mail address not masked')
-    return email_from_str(unmasked), was_masked
 
+# cachet converter
+# ----------------
 
-def masked_email_to_str(value, mask='(at)'):
-    """Value is a tuple. The first entry is the email address, the second one is a boolean telling whether the
-    email address should be masked."""
-    email, do_masking = value
-    email = email_to_str(email)
-    if do_masking:
-        email = email.replace('@', mask)
-    return email
+CACHET_REGEXP = [r'(Tiroler Naturrodelbahn-Gütesiegel) ([12]\d{3}) (leicht|mittel|schwer)$']
 
 
-def emails_from_str(value):
-    return opt_no_german_from_str(value, lambda val: enum_from_str(val, lambda v: value_comment_from_str(v, masked_email_from_str, opt_str_from_str, True)), False, [], None)
+def single_cachet_german_from_str(value: str) -> Tuple[str, str, str]:
+    for pattern in CACHET_REGEXP:
+        match_ = re.match(pattern, value)
+        if match_:
+            return match_.groups()
+    raise ValueError(f"'{value}' is no valid cachet")
 
 
-def emails_to_str(value):
-    return opt_no_german_to_str(value, lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, masked_email_to_str, opt_str_to_str, True)), False, [], None)
+def single_cachet_german_to_str(value: Tuple[str, str, str]) -> str:
+    return ' '.join(value)
 
 
-class MaskedEmail(formencode.FancyValidator):
-    """A masked email address as defined here is an email address that has the `@` character replacted by the text `(at)`.
-    So instead of `abd.def@example.com` it would be `abc.def(at)example.com`.
-    This validator takes either a normal or a masked email address in it's to_python method and returns the normal email address as well
-    as a bool indicating whether the email address was masked.
-    u''                       <=> (None, None)
-    u'abc.def@example.com'    <=> (u'abc.def@example.com', False)
-    u'abc.def(at)example.com' <=> (u'abc.def@example.com', True)
-    
-    """
-    def __init__(self, *args, **kw):
-        if 'strip' not in kw: kw['strip'] = True
-        if 'not_empty' not in kw: kw['not_empty'] = False
-        if 'if_empty' not in kw: kw['if_empty'] = (None, None)
-        self.at = '(at)'
-        formencode.FancyValidator.__init__(self, *args, **kw)
-
-    def _to_python(self, value, state=None):
-        email = value.replace(self.at, '@')
-        masked = value != email
-        val_email = formencode.validators.Email()
-        return val_email.to_python(email, state), masked
-
-    def _from_python(self, value, state=None):
-        email, masked = value
-        if email is None: return ''
-        val_email = formencode.validators.Email()
-        email = val_email.from_python(email, state)
-        if masked: email = email.replace('@', self.at)
-        return email
-
-
-'''
-class EmailCommentListNeinLoopNone(NoneValidator):
-    """Converts a semicolon-separated list of email addresses with optional comments to itself.
-    The special value of u'Nein' indicates that there are no email addresses.
-    The empty string translates to None:
-    u''                                                   <=> None
-    u'Nein'                                               <=> u'Nein'
-    u'first@example.com'                                  <=> u'first@example.com'
-    u'first@example.com (Nur Winter); second@example.com' <=> u'first@example.com (Nur Winter); second@example.com'
-
-    If the parameter allow_masked_email is true, the following gives no error:
-    u'abc.def(at)example.com (comment)'                   <=> u'abc.def(at)example.com (comment)'
-    """
-    def __init__(self, allow_masked_email=False):
-        NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(MaskedEmail() if allow_masked_email else formencode.validators.Email()))))
-'''
+def cachet_german_from_str(value) -> Optional[List[Tuple[str, str, str]]]:
+    """Converts a "Gütesiegel":
+    '' => None
+    'Nein' => []
+    'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' => [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]"""
+    return opt_no_german_from_str(value, lambda val: enum_from_str(val, single_cachet_german_from_str), False, [], None)
 
-class WikiPage(formencode.FancyValidator):
-    """Validates wiki page name like u'[[Birgitzer Alm]]'.
-    The page is not checked for existance.
-    An empty string is an error.
-    u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
-    """
-    def to_python(self, value, state=None):
-        self.assert_string(value, state)
-        if not value.startswith('[[') or not value.endswith(']]'): 
-            raise formencode.Invalid('No valid wiki page name', value, state)
-        return value
-    
-    def from_python(self, value, state=None):
-        return value
-
-
-'''
-class WikiPageList(SemicolonList):
-    """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]'.
-    u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> [u'[[Birgitzer Alm]]', u'[[Kemater Alm]]']
-    u'[[Birgitzer Alm]]'                  <=> [u'[[Birgitzer Alm]]']
-    u''                                   <=> []
-    """
-    def __init__(self):
-        SemicolonList.__init__(self, WikiPage())
-'''
 
+def cachet_german_to_str(value):
+    return opt_no_german_to_str(value, lambda val: enum_to_str(val, single_cachet_german_to_str), False, [], None)
 
-'''
-class WikiPageListLoopNone(NoneValidator):
-    """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]' as string.
-    u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> u'[[Birgitzer Alm]]; [[Kemater Alm]]'
-    u'[[Birgitzer Alm]]'                  <=> u'[[Birgitzer Alm]]'
-    u''                                   <=> None
-    """
-    def __init__(self):
-        NoneValidator.__init__(self, Loop(WikiPageList()))
-'''
 
+cachet_german_converter = FromToConverter(cachet_german_from_str, cachet_german_to_str)
 
-LIFT_GERMAN = ['Sessellift', 'Gondel', 'Linienbus', 'Taxi', 'Sonstige']
 
+# night light days converter
+# --------------------------
 
-def lift_german_from_str(value):
-    """Checks a lift_details property. It is a value comment property with the following
-    values allowed:
-    'Sessellift'
-    'Gondel'
-    'Linienbus'
-    'Taxi'
-    'Sonstige'
-    Alternatively, the value u'Nein' is allowed.
-    An empty string maps to (None, None).
+def nightlightdays_from_str(value: str) -> Tuple[int, Optional[str]]:
+    return value_comment_from_str(
+        value,
+        lambda val:
+            opt_from_str(val, lambda v: int_from_str(v, minimum=0, maximum=7)), opt_str_from_str, comment_optional=True)
 
-    Examples:
-    ''                                       <=> None
-    'Nein'                                   <=> []
-    'Sessellift                              <=> [('Sessellift', None)]
-    'Gondel (nur bis zur Hälfte)'            <=> [('Gondel', 'nur bis zur Hälfte')]
-    'Sessellift; Taxi'                       <=> [('Sessellift', None), ('Taxi', None)]
-    'Sessellift (Wochenende); Taxi (6 Euro)' <=> [('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')]
-    """
-    return opt_no_german_from_str(value, lambda value_enum: enum_from_str(value_enum, lambda value_comment: value_comment_from_str(value_comment, lambda v: choice_from_str(v, LIFT_GERMAN), opt_str_from_str, comment_optional=True)), use_tuple=False, no_value=[], none=None)
 
+def nightlightdays_to_str(value: Tuple[int, Optional[str]]) -> str:
+    return value_comment_to_str(value, lambda val: opt_to_str(val, int_to_str), opt_str_to_str, comment_optional=True)
 
-def lift_german_to_str(value):
-    return opt_no_german_to_str(value, lambda value_enum: enum_to_str(value_enum, lambda value_comment: value_comment_to_str(value_comment, str_to_str, opt_str_to_str, comment_optional=True)), use_tuple=False, no_value=[], none=None)
 
+nightlightdays_converter = FromToConverter(nightlightdays_from_str, nightlightdays_to_str)
 
-lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str)
 
+# string with optional comment enum/list converter
+# ------------------------------------------------
 
-def sledrental_from_str(value):
+def opt_str_opt_comment_enum_from_str(value: str) -> Optional[List[Tuple[str, Optional[str]]]]:
     """The value can be an empty string, 'Nein' or a semicolon-separated list of strings with optional comments.
     ''                                       => None
     'Nein'                                   => []
     'Talstation (nur mit Ticket); Schneealm' => [('Talstation', 'nur mit Ticket'), ('Schneealm', None)]"""
-    return opt_no_german_from_str(value, lambda val: enum_from_str(val, lambda v: value_comment_from_str(v, req_str_from_str, opt_str_from_str, True)), False, [], None)
+    return opt_no_german_from_str(
+        value,
+        lambda val:
+            enum_from_str(
+                val,
+                lambda v:
+                    value_comment_from_str(v, req_str_from_str, opt_str_from_str, True)
+            ), False, [], None)
+
 
+def opt_str_opt_comment_enum_to_str(value: Optional[List[Tuple[str, Optional[str]]]]) -> str:
+    return opt_no_german_to_str(
+        value,
+        lambda val:
+            enum_to_str(
+                val, lambda v:
+                    value_comment_to_str(v, str_to_str, opt_str_to_str, True)
+            ), False, [], None)
 
-def sledrental_to_str(value):
-    return opt_no_german_to_str(value, lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, str_to_str, opt_str_to_str, True)), False, [], None)
 
+opt_str_opt_comment_enum_converter = FromToConverter(opt_str_opt_comment_enum_from_str, opt_str_opt_comment_enum_to_str)
 
-sledrental_converter = FromToConverter(sledrental_from_str, sledrental_to_str)
 
+# wikibox converter
+# -----------------
 
 class ValueErrorList(ValueError):
     pass
 
 
-def box_from_template(template, name, converter_dict):
-    if template.name.strip() != name:
-        raise ValueError('Box name has to be "{}"'.format(name))
+def wikibox_from_template(template: Template, converter_dict: dict) -> dict:
+    """Returns an ordered dict."""
     result = OrderedDict()
     exceptions_dict = OrderedDict()
     # check values
     for key, converter in converter_dict.items():
         try:
             if not template.has(key):
-                raise ValueError('Missing parameter "{}"'.format(key))
+                raise ValueError(f'Missing parameter "{key}"')
             result[key] = converter.from_str(str(template.get(key).value.strip()))
         except ValueError as e:
             exceptions_dict[key] = e
     # check if keys are superfluous
     superfluous_keys = {str(p.name.strip()) for p in template.params} - set(converter_dict.keys())
     for key in superfluous_keys:
-        exceptions_dict[key] = ValueError('Superfluous parameter: "{}"'.format(key))
+        exceptions_dict[key] = ValueError(f'Superfluous parameter: "{key}"')
     if len(exceptions_dict) > 0:
-        raise ValueErrorList('{} error(s) occurred when parsing template parameters.'.format(len(exceptions_dict)), exceptions_dict)
+        raise ValueErrorList(f'{len(exceptions_dict)} error(s) occurred when parsing template parameters.',
+                             exceptions_dict)
     return result
 
 
-def box_to_template(value, name, converter_dict):
-    template = mwparserfromhell.nodes.template.Template(name)
+def wikibox_to_template(value: dict, name: str, converter_dict: dict) -> Template:
+    template = Template(name)
     for key, converter in converter_dict.items():
         template.add(key, converter.to_str(value[key]))
     return template
 
 
-def template_from_str(value, name):
+def template_from_str(value: str, name: str) -> Template:
     wikicode = mwparserfromhell.parse(value)
-    template_list = wikicode.filter_templates(name)
-    if len(name) == 0:
-        raise ValueError('No "{}" template was found'.format(name))
+    template_list = wikicode.filter_templates(recursive=False, matches=lambda t: t.name.strip() == name)
+    if len(template_list) == 0:
+        raise ValueError(f'No "{name}" template was found')
     if len(template_list) > 1:
-        raise ValueError('{} "{}" templates were found'.format(len(template_list), name))
+        raise ValueError(f'{len(template_list)} "{name}" templates were found')
     return template_list[0]
 
 
-def box_from_str(value, name, converter_dict):
+def wikibox_from_str(value: str, name: str, converter_dict: dict) -> dict:
     template = template_from_str(value, name)
-    return box_from_template(template, name, converter_dict)
+    return wikibox_from_template(template, converter_dict)
+
 
+def wikibox_to_str(value: dict, name: str, converter_dict: dict) -> str:
+    return str(wikibox_to_template(value, name, converter_dict))
 
-def box_to_str(value, name, converter_dict):
-    return str(box_to_template(value, name, converter_dict))
 
+# Rodelbahnbox converter
+# ----------------------
 
 RODELBAHNBOX_TEMPLATE_NAME = 'Rodelbahnbox'
 
 
 RODELBAHNBOX_DICT = OrderedDict([
-    ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
-    ('Position oben', opt_lonlat_converter), # '47.583333 N 15.75 E'
-    ('Höhe oben', opt_meter_converter), # '2000'
-    ('Position unten', opt_lonlat_converter), # '47.583333 N 15.75 E'
-    ('Höhe unten', opt_meter_converter), # '1200'
-    ('Länge', opt_meter_converter), # 3500
-    ('Schwierigkeit', opt_difficulty_german_converter), # 'mittel'
-    ('Lawinen', opt_avalanches_german_converter), # 'kaum'
-    ('Betreiber', opt_str_converter), # 'Max Mustermann'
-    ('Öffentliche Anreise', opt_public_transport_german_converter), # 'Mittelmäßig'
-    ('Aufstieg möglich', opt_bool_german_converter), # 'Ja'
-    ('Aufstieg getrennt', opt_tristate_german_comment_converter), # 'Ja'
-    ('Gehzeit', opt_minutes_converter), # 90
-    ('Aufstiegshilfe', lift_german_converter), # 'Gondel (unterer Teil)'
+    ('Position', opt_lonlat_converter),  # '47.583333 N 15.75 E'
+    ('Position oben', opt_lonlat_converter),  # '47.583333 N 15.75 E'
+    ('Höhe oben', opt_uint_converter),  # '2000'
+    ('Position unten', opt_lonlat_converter),  # '47.583333 N 15.75 E'
+    ('Höhe unten', opt_uint_converter),  # '1200'
+    ('Länge', opt_uint_converter),  # 3500
+    ('Schwierigkeit', opt_difficulty_german_converter),  # 'mittel'
+    ('Lawinen', opt_avalanches_german_converter),  # 'kaum'
+    ('Betreiber', opt_no_or_str_converter),  # 'Max Mustermann'
+    ('Öffentliche Anreise', opt_public_transport_german_converter),  # 'Mittelmäßig'
+    ('Aufstieg möglich', opt_bool_german_converter),  # 'Ja'
+    ('Aufstieg getrennt', opt_tristate_german_comment_converter),  # 'Ja'
+    ('Gehzeit', opt_uint_converter),  # 90
+    ('Aufstiegshilfe', lift_german_converter),  # 'Gondel (unterer Teil)'
     ('Beleuchtungsanlage', opt_tristate_german_comment_converter),
-    ('Beleuchtungstage', nightlightdays_converter), # '3 (Montag, Mittwoch, Freitag)'
-    ('Rodelverleih', sledrental_converter), # 'Talstation Serlesbahnan'
-    ('Gütesiegel', cachet_german_converter), # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
-    ('Webauskunft', webauskunft_converter), # 'http://www.nösslachhütte.at/page9.php'
-    ('Telefonauskunft', telefonauskunft_converter), # '+43-664-5487520 (Mitterer Alm)'
+    ('Beleuchtungstage', nightlightdays_converter),  # '3 (Montag, Mittwoch, Freitag)'
+    ('Rodelverleih', opt_str_opt_comment_enum_converter),  # 'Talstation Serlesbahnan'
+    ('Gütesiegel', cachet_german_converter),  # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
+    ('Webauskunft', webauskunft_converter),  # 'http://www.nösslachhütte.at/page9.php'
+    ('Telefonauskunft', opt_phone_comment_enum_converter),  # '+43-664-5487520 (Mitterer Alm)'
     ('Bild', opt_str_converter),
-    ('In Übersichtskarte', opt_bool_german_converter),
-    ('Forumid', opt_int_converter)
+    ('In Übersichtskarte', opt_bool_german_converter),
+    ('Forumid', opt_uint_converter)
 ])
 
 
-def rodelbahnbox_from_template(template):
-    return box_from_template(template, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
+def rodelbahnbox_from_template(template: Template) -> dict:
+    """Returns an ordered dict."""
+    return wikibox_from_template(template, RODELBAHNBOX_DICT)
 
 
-def rodelbahnbox_to_template(value):
-    return box_to_template(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
+def rodelbahnbox_to_template(value: dict) -> Template:
+    return wikibox_to_template(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
 
 
-def rodelbahnbox_from_str(value):
-    return box_from_str(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
+def rodelbahnbox_from_str(value: str) -> Dict:
+    """Returns an ordered dict."""
+    return wikibox_from_str(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
 
 
-def rodelbahnbox_to_str(value):
+def rodelbahnbox_to_str(value: Dict) -> str:
     template = rodelbahnbox_to_template(value)
-    template_to_table(template, 20)
+    format_template_table(template, 20)
     return str(template)
 
 
+# Gasthausbox converter
+# ---------------------
+
 GASTHAUSBOX_TEMPLATE_NAME = 'Gasthausbox'
 
 
-'''
 GASTHAUSBOX_DICT = OrderedDict([
-    ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
-    ('Höhe', opt_meter_converter),
+    ('Position', opt_lonlat_converter),  # '47.583333 N 15.75 E'
+    ('Höhe', opt_uint_converter),
     ('Betreiber', opt_str_converter),
-    ('Sitzplätze', opt_int_converter),
-    ('Übernachtung', BoolUnicodeTupleValidator()),
-    ('Rauchfrei', opt_tristate_german_validator),
-    ('Rodelverleih', BoolUnicodeTupleValidator()),
-    ('Handyempfang', ValueCommentListNeinLoopNone()),
+    ('Sitzplätze', opt_uint_converter),
+    ('Übernachtung', opt_no_or_str_converter),
+    ('Rauchfrei', opt_tristate_german_converter),
+    ('Rodelverleih', opt_no_or_str_converter),
+    ('Handyempfang', opt_str_opt_comment_enum_converter),
     ('Homepage', webauskunft_converter),
-    ('E-Mail', EmailCommentListNeinLoopNone(allow_masked_email=True)),
-    ('Telefon', PhoneCommentListNeinLoopNone(comments_are_optional=True)),
+    ('E-Mail', emails_converter),
+    ('Telefon', opt_phone_comment_opt_enum_converter),
     ('Bild', opt_str_converter),
-    ('Rodelbahnen', WikiPageListLoopNone())])
-'''
+    ('Rodelbahnen', opt_wikipage_enum_converter)])
+
+
+def gasthausbox_from_template(template: Template) -> dict:
+    """Returns an ordered dict."""
+    return wikibox_from_template(template, GASTHAUSBOX_DICT)
+
+
+def gasthausbox_to_template(value: dict) -> Template:
+    return wikibox_to_template(value, GASTHAUSBOX_TEMPLATE_NAME, GASTHAUSBOX_DICT)
+
+
+def gasthausbox_from_str(value: str) -> dict:
+    """Returns an ordered dict."""
+    return wikibox_from_str(value, GASTHAUSBOX_TEMPLATE_NAME, GASTHAUSBOX_DICT)
+
+
+def gasthausbox_to_str(value: dict) -> str:
+    template = gasthausbox_to_template(value)
+    format_template_table(template, 17)
+    return str(template)
+
 
+# Helper function to make page title pretty
+# -----------------------------------------
 
-def sledrun_page_title_to_pretty_url(page_title):
+def sledrun_page_title_to_pretty_url(page_title: str) -> str:
     """Converts a page_title from the page_title column of wrsledruncache to name_url.
     name_url is not used by MediaWiki but by new applications like wrweb."""
     return page_title.lower().replace(' ', '-').replace('_', '-').replace('(', '').replace(')', '')