X-Git-Url: https://git.toastfreeware.priv.at/philipp/winterrodeln/wrpylib.git/blobdiff_plain/ebd14c98b1238a008b6f38d0f00e3eb13b157cda..c6d5b01aec149831e4900df8048df8d15ab7d566:/wrpylib/wrvalidators.py diff --git a/wrpylib/wrvalidators.py b/wrpylib/wrvalidators.py index d311455..0a18088 100644 --- a/wrpylib/wrvalidators.py +++ b/wrpylib/wrvalidators.py @@ -2,357 +2,244 @@ # -*- 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 ' N 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): @@ -449,149 +336,223 @@ 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): @@ -614,43 +575,41 @@ 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): @@ -685,6 +644,7 @@ 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. @@ -699,7 +659,7 @@ class EmailCommentListNeinLoopNone(NoneValidator): """ 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]]'. @@ -725,8 +685,10 @@ class WikiPageList(SemicolonList): """ 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]]' @@ -735,113 +697,175 @@ class WikiPageListLoopNone(NoneValidator): """ 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):