2 # -*- coding: iso-8859-15 -*-
6 A converter is a Python variable (may be a class, class instance or anything else) that has the member
7 functions from_str and to_str. From string takes a string "from the outside", checks it and returns a Python variable
8 representing that value in Python. It reports error by raising ValueError. to_str does the opposite, however, it
9 can assume that the value it has to convert to a string is valid. If it gets an invalid value, the behavior is
15 import xml.dom.minidom as minidom
16 from xml.parsers.expat import ExpatError
17 from collections import OrderedDict, namedtuple
19 import mwparserfromhell
21 import formencode.national
22 from wrpylib.mwmarkup import template_to_table
25 class OrderedSchema(formencode.Schema):
26 def _convert_to_python(self, value, state):
27 pre_validators = self.pre_validators
28 chained_validators = self.chained_validators
29 for validator in pre_validators:
30 value = validator.to_python(value, state)
31 self.pre_validators = []
32 self.chained_validators = []
34 result = formencode.Schema._convert_to_python(self, value, state)
35 ordered_result = OrderedDict()
36 for key in value.keys():
37 ordered_result[key] = result[key]
38 for validator in chained_validators:
39 ordered_result = validator.to_python(ordered_result, state)
41 self.pre_validators = pre_validators
42 self.chained_validators = chained_validators
45 def _convert_from_python(self, value, state):
46 # store original pre- and chained validators
47 pre_validators = self.pre_validators
48 chained_validators = self.chained_validators[:]
49 # apply chained validators
50 chained = chained_validators[:]
52 for validator in chained:
53 value = validator.from_python(value, state)
54 # tempoarly remove pre- and chained validators
55 self.pre_validators = []
56 self.chained_validators = []
57 # apply original _convert_from_python method
59 result = formencode.Schema._convert_from_python(self, value, state)
60 ordered_result = OrderedDict()
61 for key in value.keys():
62 ordered_result[key] = result[key]
63 # apply pre_validators
64 pre = pre_validators[:]
67 ordered_result = validator.from_python(ordered_result, state)
69 # resore original pre- and chained_validators
70 self.pre_validators = pre_validators
71 self.chained_validators = chained_validators
75 class NoneValidator(formencode.FancyValidator):
76 """Takes a validator and makes it possible that empty strings are mapped to None."""
77 def __init__(self, validator, python_none=None):
78 self.validator = validator
79 self.python_none = python_none
81 def to_python(self, value, state=None):
82 self.assert_string(value, state)
83 if value == '': return self.python_none
84 return self.validator.to_python(value, state)
86 def from_python(self, value, state=None):
87 if value == self.python_none: return ''
88 return self.validator.from_python(value, state)
91 class NeinValidator(formencode.FancyValidator):
92 """Take an arbitrary validator and adds the possibility that the
93 string can be u'Nein'.
94 Example together with an UnsignedNone validator:
95 >>> v = NeinValidator(UnsignedNone())
98 >>> v.to_python(u'34')
100 >>> v.to_python(u'Nein')
103 def __init__(self, validator, python_no='Nein'):
104 self.validator = validator
105 self.python_no = python_no
107 def to_python(self, value, state=None):
108 self.assert_string(value, state)
109 if value == 'Nein': return self.python_no
110 return self.validator.to_python(value, state)
112 def from_python(self, value, state=None):
113 if value == self.python_no: return 'Nein'
114 return self.validator.from_python(value, state)
117 class Unicode(formencode.FancyValidator):
118 """Converts an unicode string to an unicode string:
119 u'any string' <=> u'any string'"""
120 def to_python(self, value, state=None):
121 self.assert_string(value, state)
124 def from_python(self, value, state=None):
128 class UnicodeNone(NoneValidator):
129 """Converts an unicode string to an unicode string:
131 u'any string' <=> u'any string'"""
133 NoneValidator.__init__(self, Unicode())
136 class Unsigned(formencode.FancyValidator):
137 """Converts an unsigned number to a string and vice versa:
142 def __init__(self, max=None):
143 self.iv = formencode.validators.Int(min=0, max=max)
145 def to_python(self, value, state=None):
146 self.assert_string(value, state)
147 return self.iv.to_python(value, state)
149 def from_python(self, value, state=None):
153 class UnsignedNone(NoneValidator):
154 """Converts an unsigned number to a string and vice versa:
160 def __init__(self, max=None):
161 NoneValidator.__init__(self, Unsigned(max))
164 class UnsignedNeinNone(NoneValidator):
165 """ Translates a number of Nein to a number.
173 NoneValidator.__init__(self, UnsignedNone())
176 class Loop(formencode.FancyValidator):
177 """Takes a validator and calls from_python(to_python(value))."""
178 def __init__(self, validator):
179 self.validator = validator
181 def to_python(self, value, state=None):
182 self.assert_string(value, state)
183 return self.validator.from_python(self.validator.to_python(value, state))
185 def from_python(self, value, state=None):
186 # we don't call self.validator.to_python(self.validator.from_python(value))
187 # here because our to_python implementation basically leaves the input untouched
188 # and so should from_python do.
189 return self.validator.from_python(self.validator.to_python(value, state))
192 class DictValidator(formencode.FancyValidator):
193 """Translates strings to other values via a python directory.
194 >>> boolValidator = DictValidator({u'': None, u'Ja': True, u'Nein': False})
195 >>> boolValidator.to_python(u'')
197 >>> boolValidator.to_python(u'Ja')
200 def __init__(self, dict):
203 def to_python(self, value, state=None):
204 self.assert_string(value, state)
205 if value not in self.dict: raise formencode.Invalid("Key not found in dict.", value, state)
206 return self.dict[value]
208 def from_python(self, value, state=None):
209 for k, v in self.dict.items():
212 raise formencode.Invalid('Invalid value', value, state)
215 class GermanBoolNone(DictValidator):
216 """Converts German bool values to the python bool type:
222 DictValidator.__init__(self, {'': None, 'Ja': True, 'Nein': False})
225 class GermanTristateTuple(DictValidator):
226 """Does the following conversion:
228 u'Ja' <=> (True, False)
229 u'Teilweise' <=> (True, True)
230 u'Nein' <=> (False, True)"""
231 def __init__(self, yes_python = (True, False), no_python = (False, True), partly_python = (True, True), none_python = (None, None)):
232 DictValidator.__init__(self, {'': none_python, 'Ja': yes_python, 'Nein': no_python, 'Teilweise': partly_python})
235 class GermanTristateFloat(GermanTristateTuple):
236 """Converts the a property with the possible values 0.0, 0.5, 1.0 or None
243 GermanTristateTuple.__init__(self, yes_python=1.0, no_python=0.0, partly_python=0.5, none_python=None)
246 class ValueComment(formencode.FancyValidator):
247 """Converts value with a potentially optional comment to a python tuple. If a comment is present, the
248 closing bracket has to be the rightmost character.
250 u'value' <=> (u'value', None)
251 u'value (comment)' <=> (u'value', u'comment')
252 u'[[link (linkcomment)]]' <=> (u'[[link (linkcomment)]]', None)
253 u'[[link (linkcomment)]] (comment)' <=> (u'[[link (linkcomment)]]', comment)
255 def __init__(self, value_validator=UnicodeNone(), comment_validator=UnicodeNone(), comment_is_optional=True):
256 self.value_validator = value_validator
257 self.comment_validator = comment_validator
258 self.comment_is_optional = comment_is_optional
260 def to_python(self, value, state=None):
261 self.assert_string(value, state)
266 right = value.rfind(')')
267 if right+1 != len(value):
268 if not self.comment_is_optional: raise formencode.Invalid('Mandatory comment not present', value, state)
272 left = value.rfind('(')
273 if left < 0: raise formencode.Invalid('Invalid format', value, state)
274 v = value[:left].strip()
275 c = value[left+1:right].strip()
276 return self.value_validator.to_python(v, state), self.comment_validator.to_python(c, state)
278 def from_python(self, value, state=None):
279 assert len(value) == 2
280 v = self.value_validator.from_python(value[0], state)
281 c = self.comment_validator.from_python(value[1], state)
283 if len(v) > 0: return '%s (%s)' % (v, c)
284 else: return '(%s)' % c
289 class SemicolonList(formencode.FancyValidator):
290 """Applies a given validator to a semicolon separated list of values and returns a python list.
291 For an empty string an empty list is returned."""
292 def __init__(self, validator=Unicode()):
293 self.validator = validator
295 def to_python(self, value, state=None):
296 self.assert_string(value, state)
297 return [self.validator.to_python(s.strip(), state) for s in value.split(';')]
299 def from_python(self, value, state=None):
300 return "; ".join([self.validator.from_python(s, state) for s in value])
303 class ValueCommentList(SemicolonList):
304 """A value-comment list looks like one of the following lines:
306 value (optional comment)
308 value1; value2 (optional comment)
309 value1 (optional comment1); value2 (optional comment2); value3 (otional comment3)
310 value1 (optional comment1); value2 (optional comment2); value3 (otional comment3)
311 This function returns the value-comment list as list of tuples:
312 [(u'value1', u'comment1'), (u'value2', None)]
313 If no comment is present, None is specified.
314 For an empty string, [] is returned."""
315 def __init__(self, value_validator=Unicode(), comments_are_optional=True):
316 SemicolonList.__init__(self, ValueComment(value_validator, comment_is_optional=comments_are_optional))
319 class GenericDateTime(formencode.FancyValidator):
320 """Converts a generic date/time information to a datetime class with a user defined format.
321 '2009-03-22 20:36:15' would be specified as '%Y-%m-%d %H:%M:%S'."""
323 def __init__(self, date_time_format = '%Y-%m-%d %H:%M:%S', **keywords):
324 formencode.FancyValidator.__init__(self, **keywords)
325 self.date_time_format = date_time_format
327 def to_python(self, value, state=None):
328 self.assert_string(value, state)
329 try: return datetime.datetime.strptime(value, self.date_time_format)
330 except ValueError as e: raise formencode.Invalid(str(e), value, state)
332 def from_python(self, value, state=None):
333 return value.strftime(self.date_time_format)
336 class DateTimeNoSec(GenericDateTime):
337 def __init__(self, **keywords):
338 GenericDateTime.__init__(self, '%Y-%m-%d %H:%M', **keywords)
341 class DateNone(NoneValidator):
342 """Converts date information to date classes with the format '%Y-%m-%d' or None."""
344 NoneValidator.__init__(self, GenericDateTime('%Y-%m-%d'))
348 # Meta converter types and functions
349 # ----------------------------------
353 def from_str(cls, value):
357 def to_str(cls, value):
361 FromToConverter = namedtuple('FromToConverter', ['from_str', 'to_str'])
364 def opt_from_str(value, from_str, none=None):
365 return none if value == '' else from_str(value)
368 def opt_to_str(value, to_str, none=None):
369 return '' if value == none else to_str(value)
372 class OptionalConverter(Converter):
373 converter = Converter
377 def from_str(cls, value):
378 return opt_from_str(value, cls.converter, cls.none)
381 def to_str(cls, value):
382 return opt_to_str(value, cls.converter, cls.none)
385 def choice_from_str(value, choices):
386 if value not in choices:
387 raise ValueError('{} is an invalid value')
391 def dictkey_from_str(value, key_str_dict):
393 return dict(list(zip(key_str_dict.values(), key_str_dict.keys())))[value]
395 raise ValueError("Invalid value '{}'".format(value))
398 def dictkey_to_str(value, key_str_dict):
400 return key_str_dict[value]
402 raise ValueError("Invalid value '{}'".format(value))
405 class DictKeyConverter(Converter):
406 key_str_dict = OrderedDict()
409 def from_str(cls, value):
410 return dictkey_from_str(value, cls.key_str_dict)
413 def to_str(cls, value):
414 return dictkey_to_str(value, cls.key_str_dict)
418 # Basic type converter functions
419 # ------------------------------
422 def str_from_str(value):
426 def str_to_str(value):
430 def opt_str_from_str(value):
431 return opt_from_str(value, str_from_str)
434 def opt_str_to_str(value):
435 return opt_to_str(value, str_to_str)
438 opt_str_converter = FromToConverter(opt_str_from_str, opt_str_to_str)
441 def req_str_from_str(value):
443 raise ValueError('missing required value')
444 return str_from_str(value)
447 class Str(Converter):
451 class OptStr(OptionalConverter):
455 def int_from_str(value, min=None, max=None):
457 if min is not None and value < min:
458 raise ValueError('{} must be >= than {}'.format(value, min))
459 if max is not None and value > max:
460 raise ValueError('{} must be <= than {}'.format(value, max))
464 def int_to_str(value):
468 def opt_int_from_str(value, min=None, max=None):
469 return opt_from_str(value, lambda val: int_from_str(val, min, max))
472 def opt_int_to_str(value):
473 return opt_to_str(value, int_to_str)
476 opt_int_converter = FromToConverter(opt_int_from_str, opt_int_to_str)
479 class Int(Converter):
484 def from_str(cls, value):
485 return int_from_str(value, cls.min, cls.max)
488 IntConverter = FromToConverter(int_from_str, int_to_str)
491 class OptInt(OptionalConverter):
495 class DateTime(Converter):
496 format='%Y-%m-%d %H:%M:%S'
499 def from_str(cls, value):
500 return datetime.datetime.strptime(value, cls.format)
503 def to_str(cls, value):
504 return value.strftime(cls.format)
510 def enum_from_str(value, from_str=req_str_from_str, separator=';', min_len=0):
511 """Semicolon separated list of entries with the same "type"."""
512 values = value.split(separator)
513 if len(values) == 1 and values[0] == '':
515 if len(values) < min_len:
516 raise ValueError('at least {} entry/entries have to be in the enumeration'.format(min_len))
517 return list(map(from_str, map(str.strip, values)))
520 def enum_to_str(value, to_str=opt_str_to_str, separator='; '):
521 return separator.join(map(to_str, value))
524 # Specific converter functions
525 # ----------------------------
527 BOOL_GERMAN = OrderedDict([(False, 'Nein'), (True, 'Ja')])
530 def bool_german_from_str(value):
531 return dictkey_from_str(value, BOOL_GERMAN)
534 def bool_german_to_str(value):
535 return dictkey_to_str(value, BOOL_GERMAN)
538 def opt_bool_german_from_str(value):
539 return opt_from_str(value, bool_german_from_str)
542 def opt_bool_german_to_str(value):
543 return opt_to_str(value, bool_german_to_str)
546 opt_bool_german_converter = FromToConverter(opt_bool_german_from_str, opt_bool_german_to_str)
549 TRISTATE_GERMAN = OrderedDict([(0.0, 'Nein'), (0.5, 'Teilweise'), (1.0, 'Ja')])
552 def tristate_german_from_str(value):
553 return dictkey_from_str(value, TRISTATE_GERMAN)
556 def tristate_german_to_str(value):
557 return dictkey_to_str(value, TRISTATE_GERMAN)
560 def opt_tristate_german_from_str(value):
561 return opt_from_str(value, tristate_german_from_str)
564 def opt_tristate_german_to_str(value):
565 return opt_to_str(value, tristate_german_to_str)
568 def meter_from_str(value):
569 return int_from_str(value, min=0)
572 def meter_to_str(value):
573 return int_to_str(value)
576 def opt_meter_from_str(value):
577 return opt_from_str(value, meter_from_str)
580 def opt_meter_to_str(value):
581 return opt_to_str(value, meter_to_str)
584 opt_meter_converter = FromToConverter(opt_meter_from_str, opt_meter_to_str)
587 def minutes_from_str(value):
588 return int_from_str(value, min=0)
591 def minutes_to_str(value):
592 return int_to_str(value)
595 def opt_minutes_from_str(value):
596 return opt_from_str(value, minutes_from_str)
599 def opt_minutes_to_str(value):
600 return opt_to_str(value, minutes_to_str)
603 opt_minutes_converter = FromToConverter(opt_minutes_from_str, opt_minutes_to_str)
606 LonLat = namedtuple('LonLat', ['lon', 'lat'])
609 lonlat_none = LonLat(None, None)
612 def lonlat_from_str(value):
613 """Converts a winterrodeln geo string like '47.076207 N 11.453553 E' (being '<latitude> N <longitude> E'
614 to the LonLat(lon, lat) named tupel."""
615 r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value)
616 if r is None: raise ValueError("Coordinates '{}' have not a format like '47.076207 N 11.453553 E'".format(value))
617 return LonLat(float(r.groups()[1]), float(r.groups()[0]))
620 def lonlat_to_str(value):
621 return '{:.6f} N {:.6f} E'.format(value.lat, value.lon)
624 def opt_lonlat_from_str(value):
625 return opt_from_str(value, lonlat_from_str, lonlat_none)
628 def opt_lonlat_to_str(value):
629 return opt_to_str(value, lonlat_to_str, lonlat_none)
632 opt_lonlat_converter = FromToConverter(opt_lonlat_from_str, opt_lonlat_to_str)
636 class MultiGeo(formencode.FancyValidator):
637 "Formats multiple coordinates, even in multiple lines to [(latitude, longitude, elevation), ...] or [(latitude, longitude, None), ...] tuplets."
639 # Valid for input_format
640 FORMAT_GUESS = 0 # guesses the input format; default for input_format
641 FORMAT_NONE = -1 # indicates missing formats
643 # Valid for input_format and output_format
644 FORMAT_GEOCACHING = 1 # e.g. "N 47° 13.692 E 011° 25.535"
645 FORMAT_WINTERRODELN = 2 # e.g. "47.222134 N 11.467211 E"
646 FORMAT_GMAPPLUGIN = 3 # e.g. "47.232922, 11.452239"
647 FORMAT_GPX = 4 # e.g. "<trkpt lat="47.181289" lon="11.408827"><ele>1090.57</ele></trkpt>"
649 input_format = FORMAT_GUESS
650 output_format = FORMAT_WINTERRODELN
651 last_input_format = FORMAT_NONE
653 def __init__(self, input_format = FORMAT_GUESS, output_format = FORMAT_WINTERRODELN, **keywords):
654 self.input_format = input_format
655 self.output_format = output_format
656 formencode.FancyValidator.__init__(self, if_empty = (None, None, None), **keywords)
658 def to_python(self, value, state=None):
659 self.assert_string(value, state)
660 input_format = self.input_format
661 if not input_format in [self.FORMAT_GUESS, self.FORMAT_GEOCACHING, self.FORMAT_WINTERRODELN, self.FORMAT_GMAPPLUGIN, self.FORMAT_GPX]:
662 raise formencode.Invalid("input_format %d is not recognized" % input_format, value, state) # Shouldn't it be an other type of runtime error?
663 lines = [line.strip() for line in value.split("\n") if len(line.strip()) > 0]
667 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GEOCACHING:
668 r = re.match('N ?(\d+)° ?(\d+\.\d+) +E ?(\d+)° ?(\d+\.\d+)', line)
671 result.append((float(g[0]) + float(g[1])/60, float(g[2]) + float(g[3])/60, None))
672 last_input_format = self.FORMAT_WINTERRODELN
675 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_WINTERRODELN:
676 r = re.match('(\d+\.\d+) N (\d+\.\d+) E', line)
678 result.append((float(r.groups()[0]), float(r.groups()[1]), None))
679 last_input_format = self.FORMAT_WINTERRODELN
682 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GMAPPLUGIN:
683 r = re.match('(\d+\.\d+), ?(\d+\.\d+)', line)
685 result.append((float(r.groups()[0]), float(r.groups()[1]), None))
686 last_input_format = self.FORMAT_GMAPPLUGIN
689 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GPX:
691 xml = minidom.parseString(line)
692 coord = xml.documentElement
693 lat = float(coord.getAttribute('lat'))
694 lon = float(coord.getAttribute('lon'))
695 try: ele = float(coord.childNodes[0].childNodes[0].nodeValue)
696 except (IndexError, ValueError): ele = None
697 result.append((lat, lon, ele))
698 last_input_format = self.FORMAT_GPX
700 except (ExpatError, IndexError, ValueError): pass
702 raise formencode.Invalid("Coordinates '%s' have no known format" % line, value, state)
706 def from_python(self, value, state=None):
707 output_format = self.output_format
709 for latitude, longitude, height in value:
710 if output_format == self.FORMAT_GEOCACHING:
712 result.append('N %02d° %02.3f E %03d° %02.3f' % (latitude, latitude % 1 * 60, longitude, longitude % 1 * 60))
714 elif output_format == self.FORMAT_WINTERRODELN:
715 result.append('%.6f N %.6f E' % (latitude, longitude))
717 elif output_format == self.FORMAT_GMAPPLUGIN:
718 result.append('%.6f, %.6f' % (latitude, longitude))
720 elif output_format == self.FORMAT_GPX:
721 if not height is None: result.append('<trkpt lat="%.6f" lon="%.6f"><ele>%.2f</ele></trkpt>' % (latitude, longitude, height))
722 else: result.append('<trkpt lat="%.6f" lon="%.6f"/>' % (latitude, longitude))
725 raise formencode.Invalid("output_format %d is not recognized" % output_format, value, state) # Shouldn't it be an other type of runtime error?
727 return "\n".join(result)
730 DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')])
733 def difficulty_german_from_str(value):
734 return dictkey_from_str(value, DIFFICULTY_GERMAN)
737 def difficulty_german_to_str(value):
738 return dictkey_to_str(value, DIFFICULTY_GERMAN)
741 def opt_difficulty_german_from_str(value):
742 return opt_from_str(value, difficulty_german_from_str)
745 def opt_difficulty_german_to_str(value):
746 return opt_to_str(value, difficulty_german_to_str)
749 opt_difficulty_german_converter = FromToConverter(opt_difficulty_german_from_str, opt_difficulty_german_to_str)
752 AVALANCHES_GERMAN = OrderedDict([(1, 'kaum'), (2, 'selten'), (3, 'gelegentlich'), (4, 'häufig')])
755 def avalanches_german_from_str(value):
756 return dictkey_from_str(value, AVALANCHES_GERMAN)
759 def avalanches_german_to_str(value):
760 return dictkey_to_str(value, AVALANCHES_GERMAN)
763 def opt_avalanches_german_from_str(value):
764 return opt_from_str(value, avalanches_german_from_str)
767 def opt_avalanches_german_to_str(value):
768 return opt_to_str(value, avalanches_german_to_str)
771 opt_avalanches_german_converter = FromToConverter(opt_avalanches_german_from_str, opt_avalanches_german_to_str)
774 PUBLIC_TRANSPORT_GERMAN = OrderedDict([(1, 'Sehr gut'), (2, 'Gut'), (3, 'Mittelmäßig'), (4, 'Schlecht'), (5, 'Nein'), (6, 'Ja')])
777 def public_transport_german_from_str(value):
778 return dictkey_from_str(value, PUBLIC_TRANSPORT_GERMAN)
781 def public_transport_german_to_str(value):
782 return dictkey_to_str(value, PUBLIC_TRANSPORT_GERMAN)
785 def opt_public_transport_german_from_str(value):
786 return opt_from_str(value, public_transport_german_from_str)
789 def opt_public_transport_german_to_str(value):
790 return opt_to_str(value, public_transport_german_to_str)
793 opt_public_transport_german_converter = FromToConverter(opt_public_transport_german_from_str, opt_public_transport_german_to_str)
796 def value_comment_from_str(value, value_from_str=str_from_str, comment_from_str=str_from_str, comment_optional=False):
797 """Makes it possible to have a mandatory comment in parenthesis at the end of the string."""
800 comment_end_pos = None
801 for i, char in enumerate(value[::-1]):
804 if open_brackets == 1:
806 if len(value[-1-comment_end_pos:].rstrip()) > 1:
807 raise ValueError('invalid characters after comment')
810 if open_brackets == 0:
811 comment = value[-i:-1-comment_end_pos]
812 value = value[:-i-1].rstrip()
815 if open_brackets > 0:
816 raise ValueError('bracket mismatch')
817 if not comment_optional:
818 raise ValueError('mandatory comment not found')
819 return value_from_str(value), comment_from_str(comment)
822 def value_comment_to_str(value, value_to_str=str_to_str, comment_to_str=str_to_str, comment_optional=False):
823 left = value_to_str(value[0])
824 comment = comment_to_str(value[1])
825 if len(comment) > 0 or not comment_optional:
826 comment = '({})'.format(comment)
829 if len(comment) == 0:
831 return '{} {}'.format(left, comment)
834 def opt_tristate_german_comment_from_str(value):
835 """Ja, Nein or Vielleicht, optionally with comment in parenthesis."""
836 return value_comment_from_str(value, opt_tristate_german_from_str, opt_str_from_str, True)
839 def opt_tristate_german_comment_to_str(value):
840 return value_comment_to_str(value, opt_tristate_german_to_str, opt_str_to_str, True)
843 opt_tristate_german_comment_converter = FromToConverter(opt_tristate_german_comment_from_str, opt_tristate_german_comment_to_str)
846 def no_german_from_str(value, from_str=req_str_from_str, use_tuple=True, no_value=None):
848 return (False, no_value) if use_tuple else no_value
849 return (True, from_str(value)) if use_tuple else from_str(value)
852 def no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None):
856 return to_str(value[1])
858 if value == no_value:
863 def opt_no_german_from_str(value, from_str=str_from_str, use_tuple=True, no_value=None, none=(None, None)):
864 return opt_from_str(value, lambda v: no_german_from_str(v, from_str, use_tuple, no_value), none)
867 def opt_no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None, none=(None, None)):
868 return opt_to_str(value, lambda v: no_german_to_str(v, to_str, use_tuple, no_value), none)
871 class GermanTristateFloatComment(ValueComment):
872 """Converts the a property with the possible values 0.0, 0.5, 1.0 or None and an optional comment
873 in parenthesis to a German text:
875 u'Ja' <=> (1.0, None)
876 u'Teilweise' <=> (0.5, None)
877 u'Nein' <=> (0.0, None)
878 u'Ja (aber schmal)' <=> (1.0, u'aber schmal')
879 u'Teilweise (oben)' <=> (0.5, u'oben')
880 u'Nein (aber breit)' <=> (0.0, u'aber breit')
883 ValueComment.__init__(self, GermanTristateFloat())
886 def night_light_from_str(value):
887 """'Beleuchtungsanlage' Tristate with optional comment:
890 'Teilweise' <=> (0.5, None)
891 'Nein' <=> (0.0, None)
892 'Ja (aber schmal)' <=> (1.0, 'aber schmal')
893 'Teilweise (oben)' <=> (0.5, 'oben')
894 'Nein (aber breit)' <=> (0.0, 'aber breit')
899 class NightLightDays(Int):
904 class OptNightLightDays(OptionalConverter):
905 converter = NightLightDays
908 def nightlightdays_from_str(value):
909 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)
912 def nightlightdays_to_str(value):
913 return value_comment_to_str(value, lambda val: opt_to_str(val, int_to_str), opt_str_to_str, comment_optional=True)
916 nightlightdays_converter = FromToConverter(nightlightdays_from_str, nightlightdays_to_str)
919 class UnsignedCommentNone(NoneValidator):
920 """Converts the a property with unsigned values an optional comment
921 in parenthesis to a text:
923 u'2 (Mo, Di)' <=> (2, u'Mo, Di')
927 def __init__(self, max=None):
928 NoneValidator.__init__(self, ValueComment(Unsigned(max=max)), (None, None))
931 CACHET_REGEXP = [r'(Tiroler Naturrodelbahn-Gütesiegel) ([12]\d{3}) (leicht|mittel|schwer)$']
934 def single_cachet_german_from_str(value):
935 for pattern in CACHET_REGEXP:
936 match = re.match(pattern, value)
938 return match.groups()
939 raise ValueError("'{}' is no valid cachet".format(value))
942 def single_cachet_german_to_str(value):
943 return ' '.join(value)
946 def cachet_german_from_str(value):
947 """Converts a "Gütesiegel":
950 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' => [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]"""
951 return opt_no_german_from_str(value, lambda val: enum_from_str(val, single_cachet_german_from_str), False, [], None)
954 def cachet_german_to_str(value):
955 return opt_no_german_to_str(value, lambda val: enum_to_str(val, single_cachet_german_to_str), False, [], None)
958 cachet_german_converter = FromToConverter(cachet_german_from_str, cachet_german_to_str)
961 def url_from_str(value):
962 result = urllib.parse.urlparse(value)
963 if result.scheme not in ['http', 'https']:
964 raise ValueError('scheme has to be http or https')
965 if not result.netloc:
966 raise ValueError('url does not contain netloc')
970 def url_to_str(value):
974 def webauskunft_from_str(value):
975 return opt_no_german_from_str(value, url_from_str)
978 def webauskunft_to_str(value):
979 return opt_no_german_to_str(value, url_to_str)
982 webauskunft_converter = FromToConverter(webauskunft_from_str, webauskunft_to_str)
985 class Url(formencode.FancyValidator):
986 """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed."""
987 # formencode 1.2.5 to formencode 1.3.0a1 sometimes raise ValueError instead of Invalid exceptions
988 # https://github.com/formencode/formencode/pull/61
989 urlv = formencode.validators.URL()
991 def to_python(self, value, state=None):
992 self.assert_string(value, state)
994 v = v.replace('ä', 'a')
995 v = v.replace('ö', 'o')
996 v = v.replace('ü', 'u')
997 v = v.replace('ß', 'ss')
998 v = self.urlv.to_python(v, state)
1001 def from_python(self, value, state=None):
1005 class UrlNeinNone(NoneValidator):
1006 """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed.
1007 The special value u"Nein" is allowed."""
1009 NoneValidator.__init__(self, NeinValidator(Url()))
1012 class ValueCommentListNeinLoopNone(NoneValidator):
1013 """Translates a semicolon separated list of values with optional comments in paranthesis or u'Nein' to itself.
1014 An empty string is translated to None:
1017 u'T-Mobile (gut); A1' <=> u'T-Mobile (gut); A1'"""
1019 NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList())))
1022 def phone_number_from_str(value):
1023 match = re.match(r'\+\d+(-\d+)*$', value)
1025 raise ValueError('invalid format of phone number - use something like +43-699-1234567')
1029 def phone_number_to_str(value):
1033 def telefonauskunft_from_str(value):
1034 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)
1037 def telefonauskunft_to_str(value):
1038 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)
1041 telefonauskunft_converter = FromToConverter(telefonauskunft_from_str, telefonauskunft_to_str)
1044 class PhoneNumber(formencode.FancyValidator):
1045 """Telefonnumber in international format, e.g. u'+43-699-1234567'"""
1046 def __init__(self, default_cc=43):
1047 self.validator = formencode.national.InternationalPhoneNumber(default_cc=lambda: default_cc)
1049 def to_python(self, value, state=None):
1050 return str(self.validator.to_python(value, state))
1052 def from_python(self, value, state=None):
1053 return self.validator.from_python(value, state)
1056 class PhoneCommentListNeinLoopNone(NoneValidator):
1057 """List with semicolon-separated phone numbers in international format with optional comment or 'Nein' as string:
1060 u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456' <=> u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456'
1062 def __init__(self, comments_are_optional):
1063 NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(PhoneNumber(default_cc=43), comments_are_optional=comments_are_optional))))
1066 class MaskedEmail(formencode.FancyValidator):
1067 """A masked email address as defined here is an email address that has the `@` character replacted by the text `(at)`.
1068 So instead of `abd.def@example.com` it would be `abc.def(at)example.com`.
1069 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
1070 as a bool indicating whether the email address was masked.
1071 u'' <=> (None, None)
1072 u'abc.def@example.com' <=> (u'abc.def@example.com', False)
1073 u'abc.def(at)example.com' <=> (u'abc.def@example.com', True)
1076 def __init__(self, *args, **kw):
1077 if 'strip' not in kw: kw['strip'] = True
1078 if 'not_empty' not in kw: kw['not_empty'] = False
1079 if 'if_empty' not in kw: kw['if_empty'] = (None, None)
1081 formencode.FancyValidator.__init__(self, *args, **kw)
1083 def _to_python(self, value, state=None):
1084 email = value.replace(self.at, '@')
1085 masked = value != email
1086 val_email = formencode.validators.Email()
1087 return val_email.to_python(email, state), masked
1089 def _from_python(self, value, state=None):
1090 email, masked = value
1091 if email is None: return ''
1092 val_email = formencode.validators.Email()
1093 email = val_email.from_python(email, state)
1094 if masked: email = email.replace('@', self.at)
1098 class EmailCommentListNeinLoopNone(NoneValidator):
1099 """Converts a semicolon-separated list of email addresses with optional comments to itself.
1100 The special value of u'Nein' indicates that there are no email addresses.
1101 The empty string translates to None:
1104 u'first@example.com' <=> u'first@example.com'
1105 u'first@example.com (Nur Winter); second@example.com' <=> u'first@example.com (Nur Winter); second@example.com'
1107 If the parameter allow_masked_email is true, the following gives no error:
1108 u'abc.def(at)example.com (comment)' <=> u'abc.def(at)example.com (comment)'
1110 def __init__(self, allow_masked_email=False):
1111 NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(MaskedEmail() if allow_masked_email else formencode.validators.Email()))))
1114 class WikiPage(formencode.FancyValidator):
1115 """Validates wiki page name like u'[[Birgitzer Alm]]'.
1116 The page is not checked for existance.
1117 An empty string is an error.
1118 u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
1120 def to_python(self, value, state=None):
1121 self.assert_string(value, state)
1122 if not value.startswith('[[') or not value.endswith(']]'):
1123 raise formencode.Invalid('No valid wiki page name', value, state)
1126 def from_python(self, value, state=None):
1130 class WikiPageList(SemicolonList):
1131 """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]'.
1132 u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> [u'[[Birgitzer Alm]]', u'[[Kemater Alm]]']
1133 u'[[Birgitzer Alm]]' <=> [u'[[Birgitzer Alm]]']
1137 SemicolonList.__init__(self, WikiPage())
1140 class WikiPageListLoopNone(NoneValidator):
1141 """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]' as string.
1142 u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> u'[[Birgitzer Alm]]; [[Kemater Alm]]'
1143 u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
1147 NoneValidator.__init__(self, Loop(WikiPageList()))
1150 class TupleSecondValidator(formencode.FancyValidator):
1151 """Does not really validate anything but puts the string through
1152 a validator in the second part of a tuple.
1153 Examples with an Unsigned() validator and the True argument:
1155 u'2' <=> (True, 2)"""
1156 def __init__(self, first=True, validator=UnicodeNone()):
1158 self.validator = validator
1160 def to_python(self, value, state=None):
1161 self.assert_string(value, state)
1162 return self.first, self.validator.to_python(value, state)
1164 def from_python(self, value, state=None):
1165 assert value[0] == self.first
1166 return self.validator.from_python(value[1], state)
1169 class BoolUnicodeTupleValidator(NoneValidator):
1170 """Translates an unparsed string or u'Nein' to a tuple:
1172 'Nein' <=> (False, None)
1173 'any text' <=> (True, 'any text')
1175 def __init__(self, validator=UnicodeNone()):
1176 NoneValidator.__init__(self, NeinValidator(TupleSecondValidator(True, validator), (False, None)), (None, None))
1179 LIFT_GERMAN = ['Sessellift', 'Gondel', 'Linienbus', 'Taxi', 'Sonstige']
1182 def lift_german_from_str(value):
1183 """Checks a lift_details property. It is a value comment property with the following
1190 Alternatively, the value u'Nein' is allowed.
1191 An empty string maps to (None, None).
1196 'Sessellift <=> [('Sessellift', None)]
1197 'Gondel (nur bis zur Hälfte)' <=> [('Gondel', 'nur bis zur Hälfte')]
1198 'Sessellift; Taxi' <=> [('Sessellift', None), ('Taxi', None)]
1199 'Sessellift (Wochenende); Taxi (6 Euro)' <=> [('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')]
1201 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)
1204 def lift_german_to_str(value):
1205 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)
1208 lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str)
1211 class GermanLift(BoolUnicodeTupleValidator):
1212 """Checks a lift_details property. It is a value comment property with the following
1219 Alternatively, the value u'Nein' is allowed.
1220 An empty string maps to (None, None).
1223 u'' <=> (None, None)
1224 u'Nein' <=> (False, None)
1225 u'Sessellift <=> (True, u'Sessellift')
1226 u'Gondel (nur bis zur Hälfte)' <=> (True, u'Gondel (nur bis zur Hälfte)')
1227 u'Sessellift; Taxi' <=> (True, u'Sessellift; Taxi')
1228 u'Sessellift (Wochenende); Taxi (6 Euro)' <=> (True, u'Sessellift (Wochenende); Taxi (6 Euro)')
1231 BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList(DictValidator({'Sessellift': 'Sessellift', 'Gondel': 'Gondel', 'Linienbus': 'Linienbus', 'Taxi': 'Taxi', 'Sonstige': 'Sonstige'}))))
1234 def sledrental_from_str(value):
1235 """The value can be an empty string, 'Nein' or a semicolon-separated list of strings with optional comments.
1238 'Talstation (nur mit Ticket); Schneealm' => [('Talstation', 'nur mit Ticket'), ('Schneealm', None)]"""
1239 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)
1242 def sledrental_to_str(value):
1243 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)
1246 sledrental_converter = FromToConverter(sledrental_from_str, sledrental_to_str)
1249 class ValueErrorList(ValueError):
1253 def box_from_template(template, name, converter_dict):
1254 if template.name.strip() != name:
1255 raise ValueError('Box name has to be "{}"'.format(name))
1256 result = OrderedDict()
1257 exceptions_dict = OrderedDict()
1259 for key, converter in converter_dict.items():
1261 if not template.has(key):
1262 raise ValueError('Missing parameter "{}"'.format(key))
1263 result[key] = converter.from_str(str(template.get(key).value.strip()))
1264 except ValueError as e:
1265 exceptions_dict[key] = e
1266 # check if keys are superfluous
1267 superfluous_keys = {str(p.name.strip()) for p in template.params} - set(converter_dict.keys())
1268 for key in superfluous_keys:
1269 exceptions_dict[key] = ValueError('Superfluous parameter: "{}"'.format(key))
1270 if len(exceptions_dict) > 0:
1271 raise ValueErrorList('{} error(s) occurred when parsing template parameters.'.format(len(exceptions_dict)), exceptions_dict)
1275 def box_to_template(value, name, converter_dict):
1276 template = mwparserfromhell.nodes.template.Template(name)
1277 for key, converter in converter_dict.items():
1278 template.add(key, converter.to_str(value[key]))
1282 def template_from_str(value, name):
1283 wikicode = mwparserfromhell.parse(value)
1284 template_list = wikicode.filter_templates(name)
1286 raise ValueError('No "{}" template was found'.format(name))
1287 if len(template_list) > 1:
1288 raise ValueError('{} "{}" templates were found'.format(len(template_list), name))
1289 return template_list[0]
1292 def box_from_str(value, name, converter_dict):
1293 template = template_from_str(value, name)
1294 return box_from_template(template, name, converter_dict)
1297 def box_to_str(value, name, converter_dict):
1298 return str(box_to_template(value, name, converter_dict))
1301 RODELBAHNBOX_TEMPLATE_NAME = 'Rodelbahnbox'
1304 RODELBAHNBOX_DICT = OrderedDict([
1305 ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
1306 ('Position oben', opt_lonlat_converter), # '47.583333 N 15.75 E'
1307 ('Höhe oben', opt_meter_converter), # '2000'
1308 ('Position unten', opt_lonlat_converter), # '47.583333 N 15.75 E'
1309 ('Höhe unten', opt_meter_converter), # '1200'
1310 ('Länge', opt_meter_converter), # 3500
1311 ('Schwierigkeit', opt_difficulty_german_converter), # 'mittel'
1312 ('Lawinen', opt_avalanches_german_converter), # 'kaum'
1313 ('Betreiber', opt_str_converter), # 'Max Mustermann'
1314 ('Öffentliche Anreise', opt_public_transport_german_converter), # 'Mittelmäßig'
1315 ('Aufstieg möglich', opt_bool_german_converter), # 'Ja'
1316 ('Aufstieg getrennt', opt_tristate_german_comment_converter), # 'Ja'
1317 ('Gehzeit', opt_minutes_converter), # 90
1318 ('Aufstiegshilfe', lift_german_converter), # 'Gondel (unterer Teil)'
1319 ('Beleuchtungsanlage', opt_tristate_german_comment_converter),
1320 ('Beleuchtungstage', nightlightdays_converter), # '3 (Montag, Mittwoch, Freitag)'
1321 ('Rodelverleih', sledrental_converter), # 'Talstation Serlesbahnan'
1322 ('Gütesiegel', cachet_german_converter), # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
1323 ('Webauskunft', webauskunft_converter), # 'http://www.nösslachhütte.at/page9.php'
1324 ('Telefonauskunft', telefonauskunft_converter), # '+43-664-5487520 (Mitterer Alm)'
1325 ('Bild', opt_str_converter),
1326 ('In Übersichtskarte', opt_bool_german_converter),
1327 ('Forumid', opt_int_converter)
1331 def rodelbahnbox_from_template(template):
1332 return box_from_template(template, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
1335 def rodelbahnbox_to_template(value):
1336 return box_to_template(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
1339 def rodelbahnbox_from_str(value):
1340 return box_from_str(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
1343 def rodelbahnbox_to_str(value):
1344 template = rodelbahnbox_to_template(value)
1345 template_to_table(template, 20)
1346 return str(template)
1349 GASTHAUSBOX_TEMPLATE_NAME = 'Gasthausbox'
1352 GASTHAUSBOX_DICT = OrderedDict([
1353 ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
1354 ('Höhe', opt_meter_converter),
1355 ('Betreiber', opt_str_converter),
1356 ('Sitzplätze', opt_int_converter),
1357 ('Übernachtung', BoolUnicodeTupleValidator()),
1358 ('Rauchfrei', opt_tristate_german_validator),
1359 ('Rodelverleih', BoolUnicodeTupleValidator()),
1360 ('Handyempfang', ValueCommentListNeinLoopNone()),
1361 ('Homepage', webauskunft_converter),
1362 ('E-Mail', EmailCommentListNeinLoopNone(allow_masked_email=True)),
1363 ('Telefon', PhoneCommentListNeinLoopNone(comments_are_optional=True)),
1364 ('Bild', opt_str_converter),
1365 ('Rodelbahnen', WikiPageListLoopNone())])
1369 def sledrun_page_title_to_pretty_url(page_title):
1370 """Converts a page_title from the page_title column of wrsledruncache to name_url.
1371 name_url is not used by MediaWiki but by new applications like wrweb."""
1372 return page_title.lower().replace(' ', '-').replace('_', '-').replace('(', '').replace(')', '')