# -*- coding: iso-8859-15 -*-
# $Id$
# $HeadURL$
-"""This file contains "validators" that convert between string and python (database) representation
-of properties used in the "Rodelbahnbox" and "Gasthausbox".
-The "to_python" method has to get a unicode argument.
+"""
+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.
"""
import datetime
+import email.headerregistry
+import urllib.parse
import re
import xml.dom.minidom as minidom
from xml.parsers.expat import ExpatError
-import collections
+from collections import OrderedDict, namedtuple
+
+import mwparserfromhell
import formencode
import formencode.national
+from wrpylib.mwmarkup import template_to_table
-class OrderedSchema(formencode.Schema):
- def _convert_to_python(self, value, state):
- pre_validators = self.pre_validators
- chained_validators = self.chained_validators
- for validator in pre_validators:
- value = validator.to_python(value, state)
- self.pre_validators = []
- self.chained_validators = []
- try:
- result = formencode.Schema._convert_to_python(self, value, state)
- ordered_result = collections.OrderedDict()
- for key in value.keys():
- ordered_result[key] = result[key]
- for validator in chained_validators:
- ordered_result = validator.to_python(ordered_result, state)
- finally:
- self.pre_validators = pre_validators
- self.chained_validators = chained_validators
- return ordered_result
-
- def _convert_from_python(self, value, state):
- # store original pre- and chained validators
- pre_validators = self.pre_validators
- chained_validators = self.chained_validators[:]
- # apply chained validators
- chained = chained_validators[:]
- chained.reverse()
- for validator in chained:
- value = validator.from_python(value, state)
- # tempoarly remove pre- and chained validators
- self.pre_validators = []
- self.chained_validators = []
- # apply original _convert_from_python method
- try:
- result = formencode.Schema._convert_from_python(self, value, state)
- ordered_result = collections.OrderedDict()
- for key in value.keys():
- ordered_result[key] = result[key]
- # apply pre_validators
- pre = pre_validators[:]
- pre.reverse()
- for validator in pre:
- ordered_result = validator.from_python(ordered_result, state)
- finally:
- # resore original pre- and chained_validators
- self.pre_validators = pre_validators
- self.chained_validators = chained_validators
- return ordered_result
-
-
-class NoneValidator(formencode.FancyValidator):
- """Takes a validator and makes it possible that empty strings are mapped to None."""
- def __init__(self, validator, python_none=None):
- self.validator = validator
- self.python_none = python_none
-
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- if value == '': return self.python_none
- return self.validator.to_python(value, state)
-
- def from_python(self, value, state=None):
- if value == self.python_none: return ''
- return self.validator.from_python(value, state)
-
-
-class NeinValidator(formencode.FancyValidator):
- """Take an arbitrary validator and adds the possibility that the
- string can be u'Nein'.
- Example together with an UnsignedNone validator:
- >>> v = NeinValidator(UnsignedNone())
- >>> v.to_python(u'')
- None
- >>> v.to_python(u'34')
- 34
- >>> v.to_python(u'Nein')
- u'Nein'
- """
- def __init__(self, validator, python_no='Nein'):
- self.validator = validator
- self.python_no = python_no
-
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- if value == 'Nein': return self.python_no
- return self.validator.to_python(value, state)
-
- def from_python(self, value, state=None):
- if value == self.python_no: return 'Nein'
- return self.validator.from_python(value, state)
+# Meta converter types and functions
+# ----------------------------------
+FromToConverter = namedtuple('FromToConverter', ['from_str', 'to_str'])
-class Unicode(formencode.FancyValidator):
- """Converts an unicode string to an unicode string:
- u'any string' <=> u'any string'"""
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- return str(value)
- def from_python(self, value, state=None):
- return str(value)
+def opt_from_str(value, from_str, none=None):
+ return none if value == '' else from_str(value)
-class UnicodeNone(NoneValidator):
- """Converts an unicode string to an unicode string:
- u'' <=> None
- u'any string' <=> u'any string'"""
- def __init__(self):
- NoneValidator.__init__(self, Unicode())
+def opt_to_str(value, to_str, none=None):
+ return '' if value == none else to_str(value)
-class Unsigned(formencode.FancyValidator):
- """Converts an unsigned number to a string and vice versa:
- u'0' <=> 0
- u'1' <=> 1
- u'45' <=> 45
- """
- def __init__(self, max=None):
- self.iv = formencode.validators.Int(min=0, max=max)
+def choice_from_str(value, choices):
+ if value not in choices:
+ raise ValueError('{} is an invalid value')
+ return value
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- return self.iv.to_python(value, state)
-
- def from_python(self, value, state=None):
- return str(value)
+def dictkey_from_str(value, key_str_dict):
+ try:
+ return dict(list(zip(key_str_dict.values(), key_str_dict.keys())))[value]
+ except KeyError:
+ raise ValueError("Invalid value '{}'".format(value))
-class UnsignedNone(NoneValidator):
- """Converts an unsigned number to a string and vice versa:
- u'' <=> None
- u'0' <=> 0
- u'1' <=> 1
- u'45' <=> 45
- """
- def __init__(self, max=None):
- NoneValidator.__init__(self, Unsigned(max))
+def dictkey_to_str(value, key_str_dict):
+ try:
+ return key_str_dict[value]
+ except KeyError:
+ raise ValueError("Invalid value '{}'".format(value))
-class UnsignedNeinNone(NoneValidator):
- """ Translates a number of Nein to a number.
- u'' <=> None
- u'Nein' <=> 0
- u'1' <=> 1
- u'2' <=> 2
- ...
- """
- def __init__(self):
- NoneValidator.__init__(self, UnsignedNone())
+# Basic type converter functions
+# ------------------------------
-class Loop(formencode.FancyValidator):
- """Takes a validator and calls from_python(to_python(value))."""
- def __init__(self, validator):
- self.validator = validator
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- return self.validator.from_python(self.validator.to_python(value, state))
-
- def from_python(self, value, state=None):
- # we don't call self.validator.to_python(self.validator.from_python(value))
- # here because our to_python implementation basically leaves the input untouched
- # and so should from_python do.
- return self.validator.from_python(self.validator.to_python(value, state))
-
-
-class DictValidator(formencode.FancyValidator):
- """Translates strings to other values via a python directory.
- >>> boolValidator = DictValidator({u'': None, u'Ja': True, u'Nein': False})
- >>> boolValidator.to_python(u'')
- None
- >>> boolValidator.to_python(u'Ja')
- True
- """
- def __init__(self, dict):
- self.dict = dict
-
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- if value not in self.dict: raise formencode.Invalid("Key not found in dict.", value, state)
- return self.dict[value]
-
- def from_python(self, value, state=None):
- for k, v in self.dict.items():
- if v == value:
- return k
- raise formencode.Invalid('Invalid value', value, state)
+def str_from_str(value):
+ return value
-class GermanBoolNone(DictValidator):
- """Converts German bool values to the python bool type:
- u'' <=> None
- u'Ja' <=> True
- u'Nein' <=> False
- """
- def __init__(self):
- DictValidator.__init__(self, {'': None, 'Ja': True, 'Nein': False})
-
-
-class GermanTristateTuple(DictValidator):
- """Does the following conversion:
- u'' <=> (None, None)
- u'Ja' <=> (True, False)
- u'Teilweise' <=> (True, True)
- u'Nein' <=> (False, True)"""
- def __init__(self, yes_python = (True, False), no_python = (False, True), partly_python = (True, True), none_python = (None, None)):
- DictValidator.__init__(self, {'': none_python, 'Ja': yes_python, 'Nein': no_python, 'Teilweise': partly_python})
-
-
-class GermanTristateFloat(GermanTristateTuple):
- """Converts the a property with the possible values 0.0, 0.5, 1.0 or None
- to a German text:
- u'' <=> None
- u'Ja' <=> 1.0
- u'Teilweise' <=> 0.5
- u'Nein' <=> 0.0"""
- def __init__(self):
- GermanTristateTuple.__init__(self, yes_python=1.0, no_python=0.0, partly_python=0.5, none_python=None)
+def str_to_str(value):
+ return value
-class ValueComment(formencode.FancyValidator):
- """Converts value with a potentially optional comment to a python tuple. If a comment is present, the
- closing bracket has to be the rightmost character.
- u'' <=> (None, None)
- u'value' <=> (u'value', None)
- u'value (comment)' <=> (u'value', u'comment')
- u'[[link (linkcomment)]]' <=> (u'[[link (linkcomment)]]', None)
- u'[[link (linkcomment)]] (comment)' <=> (u'[[link (linkcomment)]]', comment)
- """
- def __init__(self, value_validator=UnicodeNone(), comment_validator=UnicodeNone(), comment_is_optional=True):
- self.value_validator = value_validator
- self.comment_validator = comment_validator
- self.comment_is_optional = comment_is_optional
-
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- if value == '':
- v = value
- c = value
- else:
- right = value.rfind(')')
- if right+1 != len(value):
- if not self.comment_is_optional: raise formencode.Invalid('Mandatory comment not present', value, state)
- v = value
- c = ''
- else:
- left = value.rfind('(')
- if left < 0: raise formencode.Invalid('Invalid format', value, state)
- v = value[:left].strip()
- c = value[left+1:right].strip()
- return self.value_validator.to_python(v, state), self.comment_validator.to_python(c, state)
+def opt_str_from_str(value):
+ return opt_from_str(value, str_from_str)
- def from_python(self, value, state=None):
- assert len(value) == 2
- v = self.value_validator.from_python(value[0], state)
- c = self.comment_validator.from_python(value[1], state)
- if len(c) > 0:
- if len(v) > 0: return '%s (%s)' % (v, c)
- else: return '(%s)' % c
- return v
-
-
-class SemicolonList(formencode.FancyValidator):
- """Applies a given validator to a semicolon separated list of values and returns a python list.
- For an empty string an empty list is returned."""
- def __init__(self, validator=Unicode()):
- self.validator = validator
-
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- return [self.validator.to_python(s.strip(), state) for s in value.split(';')]
-
- def from_python(self, value, state=None):
- return "; ".join([self.validator.from_python(s, state) for s in value])
-
-
-class ValueCommentList(SemicolonList):
- """A value-comment list looks like one of the following lines:
- value
- value (optional comment)
- value1; value2
- value1; value2 (optional comment)
- value1 (optional comment1); value2 (optional comment2); value3 (otional comment3)
- value1 (optional comment1); value2 (optional comment2); value3 (otional comment3)
- This function returns the value-comment list as list of tuples:
- [(u'value1', u'comment1'), (u'value2', None)]
- If no comment is present, None is specified.
- For an empty string, [] is returned."""
- def __init__(self, value_validator=Unicode(), comments_are_optional=True):
- SemicolonList.__init__(self, ValueComment(value_validator, comment_is_optional=comments_are_optional))
-
-
-class GenericDateTime(formencode.FancyValidator):
- """Converts a generic date/time information to a datetime class with a user defined format.
- '2009-03-22 20:36:15' would be specified as '%Y-%m-%d %H:%M:%S'."""
-
- def __init__(self, date_time_format = '%Y-%m-%d %H:%M:%S', **keywords):
- formencode.FancyValidator.__init__(self, **keywords)
- self.date_time_format = date_time_format
-
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- try: return datetime.datetime.strptime(value, self.date_time_format)
- except ValueError as e: raise formencode.Invalid(str(e), value, state)
-
- def from_python(self, value, state=None):
- return value.strftime(self.date_time_format)
+def opt_str_to_str(value):
+ return opt_to_str(value, str_to_str)
-class DateTimeNoSec(GenericDateTime):
- def __init__(self, **keywords):
- GenericDateTime.__init__(self, '%Y-%m-%d %H:%M', **keywords)
+opt_str_converter = FromToConverter(opt_str_from_str, opt_str_to_str)
-class DateNone(NoneValidator):
- """Converts date information to date classes with the format '%Y-%m-%d' or None."""
- def __init__(self):
- NoneValidator.__init__(self, GenericDateTime('%Y-%m-%d'))
+def req_str_from_str(value):
+ if value == '':
+ raise ValueError('missing required value')
+ return str_from_str(value)
-class Geo(formencode.FancyValidator):
- """Formats to coordinates '47.076207 N 11.453553 E' to the (latitude, longitude) tuplet."""
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value)
- if r is None: raise formencode.Invalid("Coordinates '%s' have not a format like '47.076207 N 11.453553 E'" % value, value, state)
- return (float(r.groups()[0]), float(r.groups()[1]))
-
- def from_python(self, value, state=None):
- latitude, longitude = value
- return '%.6f N %.6f E' % (latitude, longitude)
+def int_from_str(value, min=None, max=None):
+ 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))
+ return value
+
+
+def int_to_str(value):
+ 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_to_str(value):
+ return opt_to_str(value, int_to_str)
+
+
+opt_int_converter = FromToConverter(opt_int_from_str, opt_int_to_str)
+
+
+IntConverter = FromToConverter(int_from_str, int_to_str)
+
+
+# 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)))
+
+
+def enum_to_str(value, to_str=opt_str_to_str, separator='; '):
+ return separator.join(map(to_str, value))
+
+
+# Specific converter functions
+# ----------------------------
+
+BOOL_GERMAN = OrderedDict([(False, 'Nein'), (True, 'Ja')])
+
+
+def bool_german_from_str(value):
+ return dictkey_from_str(value, BOOL_GERMAN)
+
+
+def bool_german_to_str(value):
+ return dictkey_to_str(value, BOOL_GERMAN)
+
+
+def opt_bool_german_from_str(value):
+ return opt_from_str(value, bool_german_from_str)
+
+
+def opt_bool_german_to_str(value):
+ 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_GERMAN = OrderedDict([(0.0, 'Nein'), (0.5, 'Teilweise'), (1.0, 'Ja')])
+
+
+def tristate_german_from_str(value):
+ return dictkey_from_str(value, TRISTATE_GERMAN)
+
+
+def tristate_german_to_str(value):
+ return dictkey_to_str(value, TRISTATE_GERMAN)
+
+
+def opt_tristate_german_from_str(value):
+ return opt_from_str(value, tristate_german_from_str)
+
+
+def opt_tristate_german_to_str(value):
+ 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)
+
+
+def minutes_to_str(value):
+ return int_to_str(value)
+
+
+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'])
+
+
+lonlat_none = LonLat(None, None)
+
+
+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]))
+
+
+def lonlat_to_str(value):
+ return '{:.6f} N {:.6f} E'.format(value.lat, value.lon)
+
+
+def opt_lonlat_from_str(value):
+ return opt_from_str(value, lonlat_from_str, lonlat_none)
+
+
+def opt_lonlat_to_str(value):
+ return opt_to_str(value, lonlat_to_str, lonlat_none)
+
+
+opt_lonlat_converter = FromToConverter(opt_lonlat_from_str, opt_lonlat_to_str)
-class GeoNone(NoneValidator):
- """Formats to coordinates '47.076207 N 11.453553 E' to the (latitude, longitude) tuplet."""
- def __init__(self):
- NoneValidator.__init__(self, Geo(), (None, None))
class MultiGeo(formencode.FancyValidator):
return "\n".join(result)
-# deprecated
-class AustrianPhoneNumber(formencode.FancyValidator):
- """
- Validates and converts phone numbers to +##/###/####### or +##/###/#######-### (having an extension)
- @param default_cc country code for prepending if none is provided, defaults to 43 (Austria)
- ::
- >>> v = AustrianPhoneNumber()
- >>> v.to_python(u'0512/12345678')
- u'+43/512/12345678'
- >>> v.to_python(u'+43/512/12345678')
- u'+43/512/12345678'
- >>> v.to_python(u'0512/1234567-89') # 89 is the extension
- u'+43/512/1234567-89'
- >>> v.to_python(u'+43/512/1234567-89')
- u'+43/512/1234567-89'
- >>> v.to_python(u'0512 / 12345678') # Exception
- >>> v.to_python(u'0512-12345678') # Exception
- """
- # Inspired by formencode.national.InternationalPhoneNumber
+DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')])
- default_cc = 43 # Default country code
- messages = {'phoneFormat': "'%%(value)s' is an invalid format. Please enter a number in the form +43/###/####### or 0###/########."}
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- m = re.match('^(?:\+(\d+)/)?([\d/]+)(?:-(\d+))?$', value)
- # This will separate
- # u'+43/512/1234567-89' => (u'43', u'512/1234567', u'89')
- # u'+43/512/1234/567-89' => (u'43', u'512/1234/567', u'89')
- # u'+43/512/1234/567' => (u'43', u'512/1234/567', None)
- # u'0512/1234567' => (None, u'0512/1234567', None)
- if m is None: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, state)
- (country, phone, extension) = m.groups()
-
- # Phone
- if phone.find('//') > -1 or phone.count('/') == 0: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, state)
-
- # Country
- if country is None:
- if phone[0] != '0': raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, state)
- phone = phone[1:]
- country = str(self.default_cc)
-
- if extension is None: return '+%s/%s' % (country, phone)
- return '+%s/%s-%s' % (country, phone, extension)
+def difficulty_german_from_str(value):
+ return dictkey_from_str(value, DIFFICULTY_GERMAN)
-# Deprecated
-class AustrianPhoneNumberNone(NoneValidator):
- def __init__(self):
- NoneValidator.__init__(self, AustrianPhoneNumber())
+def difficulty_german_to_str(value):
+ return dictkey_to_str(value, DIFFICULTY_GERMAN)
-# Deprecated
-class AustrianPhoneNumberCommentLoop(NoneValidator):
- def __init__(self):
- NoneValidator.__init__(self, Loop(ValueComment(AustrianPhoneNumber())))
+def opt_difficulty_german_from_str(value):
+ return opt_from_str(value, difficulty_german_from_str)
-class GermanDifficulty(DictValidator):
- """Converts the difficulty represented in a number from 1 to 3 (or None)
- to a German representation:
- u'' <=> None
- u'leicht' <=> 1
- u'mittel' <=> 2
- u'schwer' <=> 3"""
- def __init__(self):
- DictValidator.__init__(self, {'': None, 'leicht': 1, 'mittel': 2, 'schwer': 3})
+def opt_difficulty_german_to_str(value):
+ return opt_to_str(value, difficulty_german_to_str)
-class GermanAvalanches(DictValidator):
- """Converts the avalanches property represented as number from 1 to 4 (or None)
- to a German representation:
- u'' <=> None
- u'kaum' <=> 1
- u'selten' <=> 2
- u'gelegentlich' <=> 3
- u'häufig' <=> 4"""
- def __init__(self):
- DictValidator.__init__(self, {'': None, 'kaum': 1, 'selten': 2, 'gelegentlich': 3, 'häufig': 4})
-
-
-class GermanPublicTransport(DictValidator):
- """Converts the public_transport property represented as number from 1 to 6 (or None)
- to a German representation:
- u'' <=> None
- u'Sehr gut' <=> 1
- u'Gut' <=> 2
- u'Mittelmäßig' <=> 3
- u'Schlecht' <=> 4
- u'Nein' <=> 5
- u'Ja' <=> 6"""
- def __init__(self):
- DictValidator.__init__(self, {'': None, 'Sehr gut': 1, 'Gut': 2, 'Mittelmäßig': 3, 'Schlecht': 4, 'Nein': 5, 'Ja': 6})
-
-
-class GermanTristateFloatComment(ValueComment):
- """Converts the a property with the possible values 0.0, 0.5, 1.0 or None and an optional comment
- in parenthesis to a German text:
- u'' <=> (None, None)
- u'Ja' <=> (1.0, None)
- u'Teilweise' <=> (0.5, None)
- u'Nein' <=> (0.0, None)
- u'Ja (aber schmal)' <=> (1.0, u'aber schmal')
- u'Teilweise (oben)' <=> (0.5, u'oben')
- u'Nein (aber breit)' <=> (0.0, u'aber breit')
- """
- def __init__(self):
- ValueComment.__init__(self, GermanTristateFloat())
+opt_difficulty_german_converter = FromToConverter(opt_difficulty_german_from_str, opt_difficulty_german_to_str)
+
+
+AVALANCHES_GERMAN = OrderedDict([(1, 'kaum'), (2, 'selten'), (3, 'gelegentlich'), (4, 'häufig')])
+
+
+def avalanches_german_from_str(value):
+ return dictkey_from_str(value, AVALANCHES_GERMAN)
+
+
+def avalanches_german_to_str(value):
+ return dictkey_to_str(value, AVALANCHES_GERMAN)
+
+
+def opt_avalanches_german_from_str(value):
+ return opt_from_str(value, avalanches_german_from_str)
+
+
+def opt_avalanches_german_to_str(value):
+ return opt_to_str(value, avalanches_german_to_str)
+
+
+opt_avalanches_german_converter = FromToConverter(opt_avalanches_german_from_str, opt_avalanches_german_to_str)
-class UnsignedCommentNone(NoneValidator):
- """Converts the a property with unsigned values an optional comment
- in parenthesis to a text:
- u'' <=> (None, None)
- u'2 (Mo, Di)' <=> (2, u'Mo, Di')
- u'7' <=> (7, None)
- u'0' <=> (0, None)
+PUBLIC_TRANSPORT_GERMAN = OrderedDict([(1, 'Sehr gut'), (2, 'Gut'), (3, 'Mittelmäßig'), (4, 'Schlecht'), (5, 'Nein'), (6, 'Ja')])
+
+
+def public_transport_german_from_str(value):
+ return dictkey_from_str(value, PUBLIC_TRANSPORT_GERMAN)
+
+
+def public_transport_german_to_str(value):
+ return dictkey_to_str(value, PUBLIC_TRANSPORT_GERMAN)
+
+
+def opt_public_transport_german_from_str(value):
+ return opt_from_str(value, public_transport_german_from_str)
+
+
+def opt_public_transport_german_to_str(value):
+ return opt_to_str(value, public_transport_german_to_str)
+
+
+opt_public_transport_german_converter = FromToConverter(opt_public_transport_german_from_str, opt_public_transport_german_to_str)
+
+
+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)
+
+
+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)
+
+
+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)
+
+
+def opt_tristate_german_comment_to_str(value):
+ return value_comment_to_str(value, opt_tristate_german_to_str, opt_str_to_str, True)
+
+
+opt_tristate_german_comment_converter = FromToConverter(opt_tristate_german_comment_from_str, opt_tristate_german_comment_to_str)
+
+
+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 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_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_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)
+
+
+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')
"""
- def __init__(self, max=None):
- NoneValidator.__init__(self, ValueComment(Unsigned(max=max)), (None, None))
+ return
+
+
+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)
-class GermanCachet(formencode.FancyValidator):
+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)
+
+
+nightlightdays_converter = FromToConverter(nightlightdays_from_str, nightlightdays_to_str)
+
+
+CACHET_REGEXP = [r'(Tiroler Naturrodelbahn-Gütesiegel) ([12]\d{3}) (leicht|mittel|schwer)$']
+
+
+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))
+
+
+def single_cachet_german_to_str(value):
+ return ' '.join(value)
+
+
+def cachet_german_from_str(value):
"""Converts a "Gütesiegel":
- u'' <=> None
- u'Nein' <=> 'Nein'
- u'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' <=> u'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'"""
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- if value == '': return None
- elif value == 'Nein': return value
- elif value.startswith('Tiroler Naturrodelbahn-Gütesiegel '):
- p = value.split(" ")
- Unsigned().to_python(p[2], state) # check if year can be parsed
- if not p[3] in ['leicht', 'mittel', 'schwer']: raise formencode.Invalid("Unbekannter Schwierigkeitsgrad", value, state)
- return value
- else: raise formencode.Invalid("Unbekanntes Gütesiegel", value, state)
+ '' => 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)
+
- def from_python(self, value, state=None):
- if value is None: return ''
- assert value != ''
- return self.to_python(value, state)
+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)
+
+
+cachet_german_converter = FromToConverter(cachet_german_from_str, cachet_german_to_str)
+
+
+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 url_to_str(value):
+ return value
+
+
+def webauskunft_from_str(value):
+ return opt_no_german_from_str(value, url_from_str)
+
+
+def webauskunft_to_str(value):
+ return opt_no_german_to_str(value, url_to_str)
+
+
+webauskunft_converter = FromToConverter(webauskunft_from_str, webauskunft_to_str)
class Url(formencode.FancyValidator):
return value
-class UrlNeinNone(NoneValidator):
- """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed.
- The special value u"Nein" is allowed."""
- def __init__(self):
- NoneValidator.__init__(self, NeinValidator(Url()))
+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
-class ValueCommentListNeinLoopNone(NoneValidator):
- """Translates a semicolon separated list of values with optional comments in paranthesis or u'Nein' to itself.
- An empty string is translated to None:
- u'' <=> None
- u'Nein' <=> u'Nein'
- u'T-Mobile (gut); A1' <=> u'T-Mobile (gut); A1'"""
- def __init__(self):
- NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList())))
+def phone_number_to_str(value):
+ return value
-class PhoneNumber(formencode.FancyValidator):
- """Telefonnumber in international format, e.g. u'+43-699-1234567'"""
- def __init__(self, default_cc=43):
- self.validator = formencode.national.InternationalPhoneNumber(default_cc=lambda: default_cc)
+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 to_python(self, value, state=None):
- return str(self.validator.to_python(value, state))
- def from_python(self, value, state=None):
- return self.validator.from_python(value, state)
+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)
+
+
+telefonauskunft_converter = FromToConverter(telefonauskunft_from_str, telefonauskunft_to_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 email_to_str(value):
+ return str(email)
-class PhoneCommentListNeinLoopNone(NoneValidator):
- """List with semicolon-separated phone numbers in international format with optional comment or 'Nein' as string:
- u'' <=> None
- u'Nein' <=> u'Nein'
- u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456' <=> u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456'
- """
- def __init__(self, comments_are_optional):
- NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(PhoneNumber(default_cc=43), comments_are_optional=comments_are_optional))))
class MaskedEmail(formencode.FancyValidator):
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.
"""
def __init__(self, allow_masked_email=False):
NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(MaskedEmail() if allow_masked_email else formencode.validators.Email()))))
-
+'''
class WikiPage(formencode.FancyValidator):
"""Validates wiki page name like u'[[Birgitzer Alm]]'.
"""
def __init__(self):
SemicolonList.__init__(self, WikiPage())
+'''
+'''
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]]'
"""
def __init__(self):
NoneValidator.__init__(self, Loop(WikiPageList()))
+'''
-class TupleSecondValidator(formencode.FancyValidator):
- """Does not really validate anything but puts the string through
- a validator in the second part of a tuple.
- Examples with an Unsigned() validator and the True argument:
- u'6' <=> (True, 6)
- u'2' <=> (True, 2)"""
- def __init__(self, first=True, validator=UnicodeNone()):
- self.first = first
- self.validator = validator
-
- def to_python(self, value, state=None):
- self.assert_string(value, state)
- return self.first, self.validator.to_python(value, state)
-
- def from_python(self, value, state=None):
- assert value[0] == self.first
- return self.validator.from_python(value[1], state)
-
-
-class BoolUnicodeTupleValidator(NoneValidator):
- """Translates an unparsed string or u'Nein' to a tuple:
- u'' <=> (None, None)
- u'Nein' <=> (False, None)
- u'any text' <=> (True, u'any text')
- """
- def __init__(self, validator=UnicodeNone()):
- NoneValidator.__init__(self, NeinValidator(TupleSecondValidator(True, validator), (False, None)), (None, None))
+LIFT_GERMAN = ['Sessellift', 'Gondel', 'Linienbus', 'Taxi', 'Sonstige']
-class GermanLift(BoolUnicodeTupleValidator):
+def lift_german_from_str(value):
"""Checks a lift_details property. It is a value comment property with the following
values allowed:
- u'Sessellift'
- u'Gondel'
- u'Linienbus'
- u'Taxi'
- u'Sonstige'
+ 'Sessellift'
+ 'Gondel'
+ 'Linienbus'
+ 'Taxi'
+ 'Sonstige'
Alternatively, the value u'Nein' is allowed.
An empty string maps to (None, None).
-
+
Examples:
- u'' <=> (None, None)
- u'Nein' <=> (False, None)
- u'Sessellift <=> (True, u'Sessellift')
- u'Gondel (nur bis zur Hälfte)' <=> (True, u'Gondel (nur bis zur Hälfte)')
- u'Sessellift; Taxi' <=> (True, u'Sessellift; Taxi')
- u'Sessellift (Wochenende); Taxi (6 Euro)' <=> (True, u'Sessellift (Wochenende); Taxi (6 Euro)')
+ '' <=> 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')]
"""
- def __init__(self):
- BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList(DictValidator({'Sessellift': 'Sessellift', 'Gondel': 'Gondel', 'Linienbus': 'Linienbus', 'Taxi': 'Taxi', 'Sonstige': 'Sonstige'}))))
-
+ 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)
-class SledRental(BoolUnicodeTupleValidator):
- """The value can be an empty string, u'Nein' or a comma-separated list of unicode strings with optional comments.
- u'' <=> (None, None)
- u'Nein' <=> (False, None)
- u'Talstation (nur mit Ticket); Schneealm' <=> (True, u'Talstation (nur mit Ticket); Schneealm')"""
- def __init__(self):
- BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList()))
+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)
-class RodelbahnboxDictValidator(OrderedSchema):
- """Takes the fields of the Rodelbahnbox as dict of strings and returns them as dict of appropriet types."""
- def __init__(self):
- self.add_field('Position', GeoNone()) # '47.583333 N 15.75 E'
- self.add_field('Position oben', GeoNone()) # '47.583333 N 15.75 E'
- self.add_field('Höhe oben', UnsignedNone()) # '2000'
- self.add_field('Position unten', GeoNone()) # '47.583333 N 15.75 E'
- self.add_field('Höhe unten', UnsignedNone()) # '1200'
- self.add_field('Länge', UnsignedNone()) # 3500
- self.add_field('Schwierigkeit', GermanDifficulty()) # 'mittel'
- self.add_field('Lawinen', GermanAvalanches()) # 'kaum'
- self.add_field('Betreiber', UnicodeNone()) # 'Max Mustermann'
- self.add_field('Öffentliche Anreise', GermanPublicTransport()) # 'Mittelmäßig'
- self.add_field('Aufstieg möglich', GermanBoolNone()) # 'Ja'
- self.add_field('Aufstieg getrennt', GermanTristateFloatComment()) # 'Ja'
- self.add_field('Gehzeit', UnsignedNone()) # 90
- self.add_field('Aufstiegshilfe', GermanLift()) # 'Gondel (unterer Teil)'
- self.add_field('Beleuchtungsanlage', GermanTristateFloatComment())
- self.add_field('Beleuchtungstage', UnsignedCommentNone(7)) # '3 (Montag, Mittwoch, Freitag)'
- self.add_field('Rodelverleih', SledRental()) # 'Talstation Serlesbahnan'
- self.add_field('Gütesiegel', GermanCachet()) # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
- self.add_field('Webauskunft', UrlNeinNone()) # 'http://www.nösslachhütte.at/page9.php'
- self.add_field('Telefonauskunft', PhoneCommentListNeinLoopNone(comments_are_optional=False)) # '+43-664-5487520 (Mitterer Alm)'
- self.add_field('Bild', UnicodeNone())
- self.add_field('In Übersichtskarte', GermanBoolNone())
- self.add_field('Forumid', UnsignedNeinNone())
-
-
-class GasthausboxDictValidator(OrderedSchema):
- """Takes the fields of the Gasthausbox as dict of strings and returns them as dict of appropriet types."""
- def __init__(self):
- self.add_field('Position', GeoNone()) # '47.583333 N 15.75 E'
- self.add_field('Höhe', UnsignedNone())
- self.add_field('Betreiber', UnicodeNone())
- self.add_field('Sitzplätze', UnsignedNone())
- self.add_field('Übernachtung', BoolUnicodeTupleValidator())
- self.add_field('Rauchfrei', GermanTristateTuple())
- self.add_field('Rodelverleih', BoolUnicodeTupleValidator())
- self.add_field('Handyempfang', ValueCommentListNeinLoopNone())
- self.add_field('Homepage', UrlNeinNone())
- self.add_field('E-Mail', EmailCommentListNeinLoopNone(allow_masked_email=True))
- self.add_field('Telefon', PhoneCommentListNeinLoopNone(comments_are_optional=True))
- self.add_field('Bild', UnicodeNone())
- self.add_field('Rodelbahnen', WikiPageListLoopNone())
+
+lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str)
+
+
+def sledrental_from_str(value):
+ """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)
+
+
+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)
+
+
+sledrental_converter = FromToConverter(sledrental_from_str, sledrental_to_str)
+
+
+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))
+ 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))
+ 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))
+ if len(exceptions_dict) > 0:
+ raise ValueErrorList('{} error(s) occurred when parsing template parameters.'.format(len(exceptions_dict)), exceptions_dict)
+ return result
+
+
+def box_to_template(value, name, converter_dict):
+ template = mwparserfromhell.nodes.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):
+ wikicode = mwparserfromhell.parse(value)
+ template_list = wikicode.filter_templates(name)
+ if len(name) == 0:
+ raise ValueError('No "{}" template was found'.format(name))
+ if len(template_list) > 1:
+ raise ValueError('{} "{}" templates were found'.format(len(template_list), name))
+ return template_list[0]
+
+
+def box_from_str(value, name, converter_dict):
+ template = template_from_str(value, name)
+ return box_from_template(template, name, converter_dict)
+
+
+def box_to_str(value, name, converter_dict):
+ return str(box_to_template(value, name, converter_dict))
+
+
+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)'
+ ('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)'
+ ('Bild', opt_str_converter),
+ ('In Übersichtskarte', opt_bool_german_converter),
+ ('Forumid', opt_int_converter)
+])
+
+
+def rodelbahnbox_from_template(template):
+ return box_from_template(template, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
+
+
+def rodelbahnbox_to_template(value):
+ return box_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_to_str(value):
+ template = rodelbahnbox_to_template(value)
+ template_to_table(template, 20)
+ return str(template)
+
+
+GASTHAUSBOX_TEMPLATE_NAME = 'Gasthausbox'
+
+
+'''
+GASTHAUSBOX_DICT = OrderedDict([
+ ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
+ ('Höhe', opt_meter_converter),
+ ('Betreiber', opt_str_converter),
+ ('Sitzplätze', opt_int_converter),
+ ('Übernachtung', BoolUnicodeTupleValidator()),
+ ('Rauchfrei', opt_tristate_german_validator),
+ ('Rodelverleih', BoolUnicodeTupleValidator()),
+ ('Handyempfang', ValueCommentListNeinLoopNone()),
+ ('Homepage', webauskunft_converter),
+ ('E-Mail', EmailCommentListNeinLoopNone(allow_masked_email=True)),
+ ('Telefon', PhoneCommentListNeinLoopNone(comments_are_optional=True)),
+ ('Bild', opt_str_converter),
+ ('Rodelbahnen', WikiPageListLoopNone())])
+'''
def sledrun_page_title_to_pretty_url(page_title):