#!/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. """ 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 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 = 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 = 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) 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) 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()) 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 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) 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)) 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()) 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) 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) 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 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) class DateTimeNoSec(GenericDateTime): def __init__(self, **keywords): GenericDateTime.__init__(self, '%Y-%m-%d %H:%M', **keywords) 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')) # Meta converter types and functions # ---------------------------------- class Converter: @classmethod def from_str(cls, value): return value @classmethod def to_str(cls, value): return str(value) FromToConverter = namedtuple('FromToConverter', ['from_str', 'to_str']) def opt_from_str(value, from_str, none=None): return none if value == '' else from_str(value) def opt_to_str(value, to_str, none=None): return '' if value == none else to_str(value) class OptionalConverter(Converter): converter = Converter none = None @classmethod def from_str(cls, value): return opt_from_str(value, cls.converter, cls.none) @classmethod def to_str(cls, value): return opt_to_str(value, cls.converter, cls.none) def choice_from_str(value, choices): if value not in choices: raise ValueError('{} is an invalid value') return 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)) def dictkey_to_str(value, key_str_dict): try: return key_str_dict[value] except KeyError: raise ValueError("Invalid value '{}'".format(value)) class DictKeyConverter(Converter): key_str_dict = OrderedDict() @classmethod def from_str(cls, value): return dictkey_from_str(value, cls.key_str_dict) @classmethod def to_str(cls, value): return dictkey_to_str(value, cls.key_str_dict) # Basic type converter functions # ------------------------------ def str_from_str(value): return value def str_to_str(value): return value def opt_str_from_str(value): return opt_from_str(value, str_from_str) def opt_str_to_str(value): 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) class Str(Converter): pass class OptStr(OptionalConverter): converter = Str 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) class Int(Converter): min = None max = None @classmethod def from_str(cls, value): return int_from_str(value, cls.min, cls.max) IntConverter = FromToConverter(int_from_str, int_to_str) class OptInt(OptionalConverter): converter = Int class DateTime(Converter): format='%Y-%m-%d %H:%M:%S' @classmethod def from_str(cls, value): return datetime.datetime.strptime(value, cls.format) @classmethod def to_str(cls, value): return value.strftime(cls.format) # 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 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. "1090.57" 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('%.2f' % (latitude, longitude, height)) else: result.append('' % (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) DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')]) def difficulty_german_from_str(value): return dictkey_from_str(value, DIFFICULTY_GERMAN) def difficulty_german_to_str(value): return dictkey_to_str(value, DIFFICULTY_GERMAN) def opt_difficulty_german_from_str(value): return opt_from_str(value, difficulty_german_from_str) def opt_difficulty_german_to_str(value): return opt_to_str(value, difficulty_german_to_str) 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) 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) 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()) 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 class NightLightDays(Int): min = 0 max = 7 class OptNightLightDays(OptionalConverter): converter = NightLightDays 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 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) 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) """ def __init__(self, max=None): NoneValidator.__init__(self, ValueComment(Unsigned(max=max)), (None, None)) 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": '' => 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 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): """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 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 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())) 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_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 def phone_number_to_str(value): return value 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 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 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 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 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): """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())))) 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()) 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())) 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: '' <=> (None, None) 'Nein' <=> (False, None) 'any text' <=> (True, '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'] 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). 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 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) lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str) class GermanLift(BoolUnicodeTupleValidator): """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' 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)') """ def __init__(self): BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList(DictValidator({'Sessellift': 'Sessellift', 'Gondel': 'Gondel', 'Linienbus': 'Linienbus', 'Taxi': 'Taxi', 'Sonstige': 'Sonstige'})))) 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): """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(')', '')