Change shabang to python3.
[philipp/winterrodeln/wrpylib.git] / wrpylib / wrvalidators.py
1 """
2 This module contains functions that convert winterrodeln specific strings (like geographic coordinates) from string
3 to appropriate python types and the other way round.
4 Functions that take strings to convert it to python types are called *_from_str(value, [...]) and are supposed to
5 validate the string. In case of errors, a ValueError (or a subclass thereof) is returned.
6 Functions that take python types and convert it to Winterrodeln strings are called *_to_str(value, [...]) and can
7 assume that the value they get is valid. If it is not, the behavior is undefined.
8 The namedtuple FromToConverter groups corresponding *_from_str and *_to_str converters.
9 """
10 import email.headerregistry
11 import urllib.parse
12 import re
13 from collections import OrderedDict, namedtuple
14
15 import mwparserfromhell  # https://github.com/earwig/mwparserfromhell
16
17 from wrpylib.mwmarkup import format_template_table
18
19
20 # FromToConverter type
21 # --------------------
22
23 # namedtuple that groups corresponding *_from_str and *_to_str functions.
24 FromToConverter = namedtuple('FromToConverter', ['from_str', 'to_str'])
25
26
27 # optional converter
28 # ------------------
29
30 def opt_from_str(value, from_str, empty=None):
31     """Makes the converter `from_str` "optional"
32     by replacing the empty string with a predefined value (default: None)."""
33     return empty if value == '' else from_str(value)
34
35
36 def opt_to_str(value, to_str, empty=None):
37     return '' if value == empty else to_str(value)
38
39
40 # "no" converter
41 # --------------
42
43 def no_german_from_str(value, from_str, use_tuple=True, no_value=None):
44     """Makes it possible to have "Nein" as special value. If use_tuple is True, a tuple is returned. The first
45     entry of the tuple is False in case the value is "Nein", otherwiese the first value is True. The second value is
46     no_value in case of the value being "Nein", otherwise it is the result of from_str(value).
47     If use_tuple is False, no_value is returned in case the value is "Nein", otherwise the result of from_str(value)."""
48     if value == 'Nein':
49         return (False, no_value) if use_tuple else no_value
50     return (True, from_str(value)) if use_tuple else from_str(value)
51
52
53 def no_german_to_str(value, to_str, use_tuple=True, no_value=None):
54     if use_tuple:
55         if not value[0]:
56             return 'Nein'
57         return to_str(value[1])
58     else:
59         if value == no_value:
60             return 'Nein'
61         return to_str(value)
62
63
64 # "optional"/"no" converter
65 # -------------------------
66
67 def opt_no_german_from_str(value, from_str, use_tuple=True, no_value=None, empty=(None, None)):
68     return opt_from_str(value, lambda v: no_german_from_str(v, from_str, use_tuple, no_value), empty)
69
70
71 def opt_no_german_to_str(value, to_str, use_tuple=True, no_value=None, empty=(None, None)):
72     return opt_to_str(value, lambda v: no_german_to_str(v, to_str, use_tuple, no_value), empty)
73
74
75 # choice converter
76 # ----------------
77
78 def choice_from_str(value, choices):
79     """Returns the value if it is a member of the choices iterable."""
80     if value not in choices:
81         raise ValueError('{} is an invalid value')
82     return value
83
84
85 # dictkey converter
86 # -----------------
87
88 def dictkey_from_str(value, key_str_dict):
89     """Returns the key of an entry in the key_str_dict if the value of the entry corresponds to the given value."""
90     try:
91         return dict(list(zip(key_str_dict.values(), key_str_dict.keys())))[value]
92     except KeyError:
93         raise ValueError("Invalid value '{}'".format(value))
94
95
96 def dictkey_to_str(value, key_str_dict):
97     try:
98         return key_str_dict[value]
99     except KeyError:
100         raise ValueError("Invalid value '{}'".format(value))
101
102
103 # enum/"list" converter
104 # ---------------------
105
106 def enum_from_str(value, from_str, separator=';', min_len=0):
107     """Semicolon separated list of entries with the same "type"."""
108     values = value.split(separator)
109     if len(values) == 1 and values[0] == '':
110         values = []
111     if len(values) < min_len:
112         raise ValueError('at least {} entry/entries have to be in the enumeration'.format(min_len))
113     return list(map(from_str, map(str.strip, values)))
114
115
116 def enum_to_str(value, to_str, separator='; '):
117     return separator.join(map(to_str, value))
118
119
120 # value/comment converter
121 # -----------------------
122
123 def value_comment_from_str(value, value_from_str, comment_from_str, comment_optional=False):
124     """Makes it possible to have a mandatory comment in parenthesis at the end of the string."""
125     comment = ''
126     if value.endswith(')'):
127         open_brackets = 0
128         for i, char in enumerate(value[::-1]):
129             if char == ')':
130                 open_brackets += 1
131             elif char == '(':
132                 open_brackets -= 1
133                 if open_brackets == 0:
134                     comment = value[-i:-1]
135                     value = value[:-i-1]
136                     if len(value) > 1 and value[-1] != ' ':
137                         raise ValueError('there has to be a space before the opening bracket of the comment')
138                     value = value[:-1]
139                     break
140         else:
141             if open_brackets > 0:
142                 raise ValueError('bracket mismatch')
143             if not comment_optional:
144                 raise ValueError('mandatory comment not found')
145     else:
146         if not comment_optional:
147             raise ValueError('mandatory comment not found in "{}"'.format(value))
148     return value_from_str(value), comment_from_str(comment)
149
150
151 def value_comment_to_str(value, value_to_str, comment_to_str, comment_optional=False):
152     left = value_to_str(value[0])
153     comment = comment_to_str(value[1])
154     if len(comment) > 0 or not comment_optional:
155         comment = '({})'.format(comment)
156     if len(left) == 0:
157         return comment
158     if len(comment) == 0:
159         return left
160     return '{} {}'.format(left, comment)
161
162
163 # string converter
164 # ----------------
165
166 def str_from_str(value):
167     """Converter that takes any string and returns it as string without validation.
168     In other words, this function does nothing and just returns its argument."""
169     return value
170
171
172 def str_to_str(value):
173     return value
174
175
176 def req_str_from_str(value):
177     if value == '':
178         raise ValueError('missing required value')
179     return str_from_str(value)
180
181
182 def opt_str_from_str(value):
183     return opt_from_str(value, str_from_str)
184
185
186 def opt_str_to_str(value):
187     return opt_to_str(value, str_to_str)
188
189
190 opt_str_converter = FromToConverter(opt_str_from_str, opt_str_to_str)
191
192
193 # optional no or string converter
194 # -------------------------------
195
196 def opt_no_or_str_from_str(value):
197     """
198     'Nein' => (False, None); 'Nur Wochenende' => (True, 'Nur Wochenende'); 'Ja' => (True, 'Ja'); '' => (None, None)"""
199     return opt_no_german_from_str(value, req_str_from_str)
200
201
202 def opt_no_or_str_to_str(value):
203     return opt_no_german_to_str(value, str_to_str)
204
205
206 opt_no_or_str_converter = FromToConverter(opt_no_or_str_from_str, opt_no_or_str_to_str)
207
208
209 # integer converter
210 # -----------------
211
212 def int_from_str(value, min=None, max=None):
213     """Converter that takes a string representation of an integer and returns the integer.
214     :param value: string representation of an integer
215     :param min: If not None, the integer has to be at least min
216     :param max: If not None, the integer has to be no more than max
217     """
218     value = int(value)
219     if min is not None and value < min:
220         raise ValueError('{} must be >= than {}'.format(value, min))
221     if max is not None and value > max:
222         raise ValueError('{} must be <= than {}'.format(value, max))
223     return value
224
225
226 def int_to_str(value):
227     return str(value)
228
229
230 def opt_int_from_str(value, min=None, max=None):
231     return opt_from_str(value, lambda val: int_from_str(val, min, max))
232
233
234 def opt_int_to_str(value):
235     return opt_to_str(value, int_to_str)
236
237
238 def opt_uint_from_str(value, min=0, max=None):
239     """Optional positive integer."""
240     return opt_int_from_str(value, min, max)
241
242
243 def opt_uint_to_str(value):
244     return opt_int_to_str(value)
245
246
247 opt_uint_converter = FromToConverter(opt_uint_from_str, opt_uint_to_str)
248
249
250 # bool converter
251 # --------------
252
253 BOOL_GERMAN = OrderedDict([(False, 'Nein'), (True, 'Ja')])
254
255
256 def bool_german_from_str(value):
257     return dictkey_from_str(value, BOOL_GERMAN)
258
259
260 def bool_german_to_str(value):
261     return dictkey_to_str(value, BOOL_GERMAN)
262
263
264 def opt_bool_german_from_str(value):
265     return opt_from_str(value, bool_german_from_str)
266
267
268 def opt_bool_german_to_str(value):
269     return opt_to_str(value, bool_german_to_str)
270
271
272 opt_bool_german_converter = FromToConverter(opt_bool_german_from_str, opt_bool_german_to_str)
273
274
275 # tristate converter
276 # ------------------
277
278 TRISTATE_GERMAN = OrderedDict([(0.0, 'Nein'), (0.5, 'Teilweise'), (1.0, 'Ja')])
279
280
281 def tristate_german_from_str(value):
282     return dictkey_from_str(value, TRISTATE_GERMAN)
283
284
285 def tristate_german_to_str(value):
286     return dictkey_to_str(value, TRISTATE_GERMAN)
287
288
289 def opt_tristate_german_from_str(value):
290     return opt_from_str(value, tristate_german_from_str)
291
292
293 def opt_tristate_german_to_str(value):
294     return opt_to_str(value, tristate_german_to_str)
295
296
297 opt_tristate_german_converter = FromToConverter(opt_tristate_german_from_str, opt_tristate_german_to_str)
298
299
300 # tristate with comment converter
301 # -------------------------------
302
303 def opt_tristate_german_comment_from_str(value):
304     """Ja, Nein or Teilweise, optionally with comment in parenthesis."""
305     return value_comment_from_str(value, opt_tristate_german_from_str, opt_str_from_str, True)
306
307
308 def opt_tristate_german_comment_to_str(value):
309     return value_comment_to_str(value, opt_tristate_german_to_str, opt_str_to_str, True)
310
311
312 opt_tristate_german_comment_converter = FromToConverter(opt_tristate_german_comment_from_str, opt_tristate_german_comment_to_str)
313
314
315 # url converter
316 # -------------
317
318 def url_from_str(value):
319     result = urllib.parse.urlparse(value)
320     if result.scheme not in ['http', 'https']:
321         raise ValueError('scheme has to be http or https')
322     if not result.netloc:
323         raise ValueError('url does not contain netloc')
324     return value
325
326
327 def url_to_str(value):
328     return value
329
330
331 # webauskunft converter
332 # ---------------------
333
334 def webauskunft_from_str(value):
335     """Converts a URL or 'Nein' to a tuple
336     'http://www.example.com/' -> (True, 'http://www.example.com/')
337     'Nein' -> (False, None)
338     '' -> (None, None)
339
340     :param value: URL or 'Nein'
341     :return: tuple
342     """
343     return opt_no_german_from_str(value, url_from_str)
344
345
346 def webauskunft_to_str(value):
347     return opt_no_german_to_str(value, url_to_str)
348
349
350 webauskunft_converter = FromToConverter(webauskunft_from_str, webauskunft_to_str)
351
352
353 # wikipage converter
354 # ------------------
355
356 def wikipage_from_str(value):
357     """Validates wiki page name like '[[Birgitzer Alm]]'.
358     The page is not checked for existence.
359     An empty string is an error.
360     '[[Birgitzer Alm]]' => '[[Birgitzer Alm]]'
361     """
362     if re.match(r'\[\[[^\[\]]+\]\]$', value) is None:
363         raise ValueError('No valid wiki page name "{}"'.format(value))
364     return value
365
366
367 def wikipage_to_str(value):
368     return value
369
370
371 def opt_wikipage_enum_from_str(value):
372     """Validates a list of wiki pages like '[[Birgitzer Alm]]; [[Kemater Alm]]'.
373     '[[Birgitzer Alm]]; [[Kemater Alm]]' => ['[[Birgitzer Alm]]', '[[Kemater Alm]]']
374     '[[Birgitzer Alm]]'                  => ['[[Birgitzer Alm]]']
375     'Nein'                               => []
376     ''                                   => None
377     """
378     return opt_no_german_from_str(value, lambda val: enum_from_str(val, wikipage_from_str), False, [], None)
379
380
381 def opt_wikipage_enum_to_str(value):
382     return opt_no_german_to_str(value, lambda val: enum_to_str(val, wikipage_to_str), False, [], None)
383
384
385 opt_wikipage_enum_converter = FromToConverter(opt_wikipage_enum_from_str, opt_wikipage_enum_to_str)
386
387
388 # email converter
389 # ---------------
390
391 def email_from_str(value):
392     """Takes an email address like 'office@example.com', checks it for correctness and returns it again as string."""
393     try:
394         email.headerregistry.Address(addr_spec=value)
395     except email.errors.HeaderParseError as e:
396         raise ValueError('Invalid email address: {}'.format(value), e)
397     return value
398
399
400 def email_to_str(value):
401     return str(value)
402
403
404 def masked_email_from_str(value, mask='(at)', masked_only=False):
405     """Converts an email address that is possibly masked. Returns a tuple. The first parameter is the un-masked
406     email address as string, the second is a boolean telling whether the address was masked."""
407     unmasked = value.replace(mask, '@')
408     was_masked = unmasked != value
409     if masked_only and not was_masked:
410         raise ValueError('E-Mail address not masked')
411     return email_from_str(unmasked), was_masked
412
413
414 def masked_email_to_str(value, mask='(at)'):
415     """Value is a tuple. The first entry is the email address, the second one is a boolean telling whether the
416     email address should be masked."""
417     email, do_masking = value
418     email = email_to_str(email)
419     if do_masking:
420         email = email.replace('@', mask)
421     return email
422
423
424 def emails_from_str(value):
425     return opt_no_german_from_str(value, lambda val: enum_from_str(val, lambda v: value_comment_from_str(v, masked_email_from_str, opt_str_from_str, True)), False, [], None)
426
427
428 def emails_to_str(value):
429     return opt_no_german_to_str(value, lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, masked_email_to_str, opt_str_to_str, True)), False, [], None)
430
431
432 emails_converter = FromToConverter(emails_from_str, emails_to_str)
433
434
435 # phone converter
436 # ---------------
437
438 def phone_number_from_str(value):
439     match = re.match(r'\+\d+(-\d+)*$', value)
440     if match is None:
441         raise ValueError('invalid format of phone number - use something like +43-699-1234567')
442     return value
443
444
445 def phone_number_to_str(value):
446     return value
447
448
449 def opt_phone_comment_enum_from_str(value, comment_optional=False):
450     return opt_no_german_from_str(value, lambda val: enum_from_str(val, lambda v: value_comment_from_str(v, phone_number_from_str, opt_str_from_str if comment_optional else req_str_from_str, comment_optional)), False, [], None)
451
452
453 def opt_phone_comment_enum_to_str(value, comment_optional=False):
454     return opt_no_german_to_str(value, lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, phone_number_to_str, opt_str_to_str if comment_optional else str_to_str, comment_optional)), False, [], None)
455
456
457 opt_phone_comment_enum_converter = FromToConverter(opt_phone_comment_enum_from_str, opt_phone_comment_enum_to_str)
458
459
460 opt_phone_comment_opt_enum_converter = FromToConverter(lambda value: opt_phone_comment_enum_from_str(value, True), lambda value: opt_phone_comment_enum_to_str(value, True))
461
462
463 # longitude/latitude converter
464 # ----------------------------
465
466 LonLat = namedtuple('LonLat', ['lon', 'lat'])
467
468
469 lonlat_none = LonLat(None, None)
470
471
472 def lonlat_from_str(value):
473     """Converts a winterrodeln geo string like '47.076207 N 11.453553 E' (being '<latitude> N <longitude> E'
474     to the LonLat(lon, lat) named  tupel."""
475     r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value)
476     if r is None: raise ValueError("Coordinates '{}' have not a format like '47.076207 N 11.453553 E'".format(value))
477     return LonLat(float(r.groups()[1]), float(r.groups()[0]))
478
479
480 def lonlat_to_str(value):
481     return '{:.6f} N {:.6f} E'.format(value.lat, value.lon)
482
483
484 def opt_lonlat_from_str(value):
485     return opt_from_str(value, lonlat_from_str, lonlat_none)
486
487
488 def opt_lonlat_to_str(value):
489     return opt_to_str(value, lonlat_to_str, lonlat_none)
490
491
492 opt_lonlat_converter = FromToConverter(opt_lonlat_from_str, opt_lonlat_to_str)
493
494
495 # difficulty converter
496 # --------------------
497
498 DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')])
499
500
501 def difficulty_german_from_str(value):
502     return dictkey_from_str(value, DIFFICULTY_GERMAN)
503
504
505 def difficulty_german_to_str(value):
506     return dictkey_to_str(value, DIFFICULTY_GERMAN)
507
508
509 def opt_difficulty_german_from_str(value):
510     return opt_from_str(value, difficulty_german_from_str)
511
512
513 def opt_difficulty_german_to_str(value):
514     return opt_to_str(value, difficulty_german_to_str)
515
516
517 opt_difficulty_german_converter = FromToConverter(opt_difficulty_german_from_str, opt_difficulty_german_to_str)
518
519
520 # avalanches converter
521 # --------------------
522
523 AVALANCHES_GERMAN = OrderedDict([(1, 'kaum'), (2, 'selten'), (3, 'gelegentlich'), (4, 'häufig')])
524
525
526 def avalanches_german_from_str(value):
527     return dictkey_from_str(value, AVALANCHES_GERMAN)
528
529
530 def avalanches_german_to_str(value):
531     return dictkey_to_str(value, AVALANCHES_GERMAN)
532
533
534 def opt_avalanches_german_from_str(value):
535     return opt_from_str(value, avalanches_german_from_str)
536
537
538 def opt_avalanches_german_to_str(value):
539     return opt_to_str(value, avalanches_german_to_str)
540
541
542 opt_avalanches_german_converter = FromToConverter(opt_avalanches_german_from_str, opt_avalanches_german_to_str)
543
544
545 # lift converter
546 # --------------
547
548 LIFT_GERMAN = ['Sessellift', 'Gondel', 'Linienbus', 'Taxi', 'Sonstige']
549
550
551 def lift_german_from_str(value):
552     """Checks a lift_details property. It is a value comment property with the following
553     values allowed:
554     'Sessellift'
555     'Gondel'
556     'Linienbus'
557     'Taxi'
558     'Sonstige'
559     Alternatively, the value u'Nein' is allowed.
560     An empty string maps to (None, None).
561
562     Examples:
563     ''                                       <=> None
564     'Nein'                                   <=> []
565     'Sessellift                              <=> [('Sessellift', None)]
566     'Gondel (nur bis zur Hälfte)'            <=> [('Gondel', 'nur bis zur Hälfte')]
567     'Sessellift; Taxi'                       <=> [('Sessellift', None), ('Taxi', None)]
568     'Sessellift (Wochenende); Taxi (6 Euro)' <=> [('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')]
569     """
570     return opt_no_german_from_str(value, lambda value_enum: enum_from_str(value_enum, lambda value_comment: value_comment_from_str(value_comment, lambda v: choice_from_str(v, LIFT_GERMAN), opt_str_from_str, comment_optional=True)), use_tuple=False, no_value=[], empty=None)
571
572
573 def lift_german_to_str(value):
574     return opt_no_german_to_str(value, lambda value_enum: enum_to_str(value_enum, lambda value_comment: value_comment_to_str(value_comment, str_to_str, opt_str_to_str, comment_optional=True)), use_tuple=False, no_value=[], empty=None)
575
576
577 lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str)
578
579
580 # public transport converter
581 # --------------------------
582
583 PUBLIC_TRANSPORT_GERMAN = OrderedDict([(1, 'Sehr gut'), (2, 'Gut'), (3, 'Mittelmäßig'), (4, 'Schlecht'), (5, 'Nein'), (6, 'Ja')])
584
585
586 def public_transport_german_from_str(value):
587     return dictkey_from_str(value, PUBLIC_TRANSPORT_GERMAN)
588
589
590 def public_transport_german_to_str(value):
591     return dictkey_to_str(value, PUBLIC_TRANSPORT_GERMAN)
592
593
594 def opt_public_transport_german_from_str(value):
595     return opt_from_str(value, public_transport_german_from_str)
596
597
598 def opt_public_transport_german_to_str(value):
599     return opt_to_str(value, public_transport_german_to_str)
600
601
602 opt_public_transport_german_converter = FromToConverter(opt_public_transport_german_from_str, opt_public_transport_german_to_str)
603
604
605 # cachet converter
606 # ----------------
607
608 CACHET_REGEXP = [r'(Tiroler Naturrodelbahn-Gütesiegel) ([12]\d{3}) (leicht|mittel|schwer)$']
609
610
611 def single_cachet_german_from_str(value):
612     for pattern in CACHET_REGEXP:
613         match = re.match(pattern, value)
614         if match:
615             return match.groups()
616     raise ValueError("'{}' is no valid cachet".format(value))
617
618
619 def single_cachet_german_to_str(value):
620     return ' '.join(value)
621
622
623 def cachet_german_from_str(value):
624     """Converts a "Gütesiegel":
625     '' => None
626     'Nein' => []
627     'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' => [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]"""
628     return opt_no_german_from_str(value, lambda val: enum_from_str(val, single_cachet_german_from_str), False, [], None)
629
630
631 def cachet_german_to_str(value):
632     return opt_no_german_to_str(value, lambda val: enum_to_str(val, single_cachet_german_to_str), False, [], None)
633
634
635 cachet_german_converter = FromToConverter(cachet_german_from_str, cachet_german_to_str)
636
637
638 # night light days converter
639 # --------------------------
640
641 def nightlightdays_from_str(value):
642     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)
643
644
645 def nightlightdays_to_str(value):
646     return value_comment_to_str(value, lambda val: opt_to_str(val, int_to_str), opt_str_to_str, comment_optional=True)
647
648
649 nightlightdays_converter = FromToConverter(nightlightdays_from_str, nightlightdays_to_str)
650
651
652 # string with optional comment enum/list converter
653 # ------------------------------------------------
654
655 def opt_str_opt_comment_enum_from_str(value):
656     """The value can be an empty string, 'Nein' or a semicolon-separated list of strings with optional comments.
657     ''                                       => None
658     'Nein'                                   => []
659     'Talstation (nur mit Ticket); Schneealm' => [('Talstation', 'nur mit Ticket'), ('Schneealm', None)]"""
660     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)
661
662
663 def opt_str_opt_comment_enum_to_str(value):
664     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)
665
666
667 opt_str_opt_comment_enum_converter = FromToConverter(opt_str_opt_comment_enum_from_str, opt_str_opt_comment_enum_to_str)
668
669
670 # wikibox converter
671 # -----------------
672
673 class ValueErrorList(ValueError):
674     pass
675
676
677 def wikibox_from_template(template, converter_dict):
678     """Returns an ordered dict."""
679     result = OrderedDict()
680     exceptions_dict = OrderedDict()
681     # check values
682     for key, converter in converter_dict.items():
683         try:
684             if not template.has(key):
685                 raise ValueError('Missing parameter "{}"'.format(key))
686             result[key] = converter.from_str(str(template.get(key).value.strip()))
687         except ValueError as e:
688             exceptions_dict[key] = e
689     # check if keys are superfluous
690     superfluous_keys = {str(p.name.strip()) for p in template.params} - set(converter_dict.keys())
691     for key in superfluous_keys:
692         exceptions_dict[key] = ValueError('Superfluous parameter: "{}"'.format(key))
693     if len(exceptions_dict) > 0:
694         raise ValueErrorList('{} error(s) occurred when parsing template parameters.'.format(len(exceptions_dict)), exceptions_dict)
695     return result
696
697
698 def wikibox_to_template(value, name, converter_dict):
699     template = mwparserfromhell.nodes.template.Template(name)
700     for key, converter in converter_dict.items():
701         template.add(key, converter.to_str(value[key]))
702     return template
703
704
705 def template_from_str(value, name):
706     wikicode = mwparserfromhell.parse(value)
707     template_list = wikicode.filter_templates(recursive=False, matches=lambda t: t.name.strip() == name)
708     if len(template_list) == 0:
709         raise ValueError('No "{}" template was found'.format(name))
710     if len(template_list) > 1:
711         raise ValueError('{} "{}" templates were found'.format(len(template_list), name))
712     return template_list[0]
713
714
715 def wikibox_from_str(value, name, converter_dict):
716     template = template_from_str(value, name)
717     return wikibox_from_template(template, converter_dict)
718
719
720 def wikibox_to_str(value, name, converter_dict):
721     return str(wikibox_to_template(value, name, converter_dict))
722
723
724 # Rodelbahnbox converter
725 # ----------------------
726
727 RODELBAHNBOX_TEMPLATE_NAME = 'Rodelbahnbox'
728
729
730 RODELBAHNBOX_DICT = OrderedDict([
731     ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
732     ('Position oben', opt_lonlat_converter), # '47.583333 N 15.75 E'
733     ('Höhe oben', opt_uint_converter), # '2000'
734     ('Position unten', opt_lonlat_converter), # '47.583333 N 15.75 E'
735     ('Höhe unten', opt_uint_converter), # '1200'
736     ('Länge', opt_uint_converter), # 3500
737     ('Schwierigkeit', opt_difficulty_german_converter), # 'mittel'
738     ('Lawinen', opt_avalanches_german_converter), # 'kaum'
739     ('Betreiber', opt_str_converter), # 'Max Mustermann'
740     ('Öffentliche Anreise', opt_public_transport_german_converter), # 'Mittelmäßig'
741     ('Aufstieg möglich', opt_bool_german_converter), # 'Ja'
742     ('Aufstieg getrennt', opt_tristate_german_comment_converter), # 'Ja'
743     ('Gehzeit', opt_uint_converter), # 90
744     ('Aufstiegshilfe', lift_german_converter), # 'Gondel (unterer Teil)'
745     ('Beleuchtungsanlage', opt_tristate_german_comment_converter),
746     ('Beleuchtungstage', nightlightdays_converter), # '3 (Montag, Mittwoch, Freitag)'
747     ('Rodelverleih', opt_str_opt_comment_enum_converter), # 'Talstation Serlesbahnan'
748     ('Gütesiegel', cachet_german_converter), # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
749     ('Webauskunft', webauskunft_converter), # 'http://www.nösslachhütte.at/page9.php'
750     ('Telefonauskunft', opt_phone_comment_enum_converter), # '+43-664-5487520 (Mitterer Alm)'
751     ('Bild', opt_str_converter),
752     ('In Übersichtskarte', opt_bool_german_converter),
753     ('Forumid', opt_uint_converter)
754 ])
755
756
757 def rodelbahnbox_from_template(template):
758     """Returns an ordered dict."""
759     return wikibox_from_template(template, RODELBAHNBOX_DICT)
760
761
762 def rodelbahnbox_to_template(value):
763     return wikibox_to_template(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
764
765
766 def rodelbahnbox_from_str(value):
767     """Returns an ordered dict."""
768     return wikibox_from_str(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
769
770
771 def rodelbahnbox_to_str(value):
772     template = rodelbahnbox_to_template(value)
773     format_template_table(template, 20)
774     return str(template)
775
776
777 # Gasthausbox converter
778 # ---------------------
779
780 GASTHAUSBOX_TEMPLATE_NAME = 'Gasthausbox'
781
782
783 GASTHAUSBOX_DICT = OrderedDict([
784     ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
785     ('Höhe', opt_uint_converter),
786     ('Betreiber', opt_str_converter),
787     ('Sitzplätze', opt_uint_converter),
788     ('Übernachtung', opt_no_or_str_converter),
789     ('Rauchfrei', opt_tristate_german_converter),
790     ('Rodelverleih', opt_no_or_str_converter),
791     ('Handyempfang', opt_str_opt_comment_enum_converter),
792     ('Homepage', webauskunft_converter),
793     ('E-Mail', emails_converter),
794     ('Telefon', opt_phone_comment_opt_enum_converter),
795     ('Bild', opt_str_converter),
796     ('Rodelbahnen', opt_wikipage_enum_converter)])
797
798
799 def gasthausbox_from_template(template):
800     """Returns an ordered dict."""
801     return wikibox_from_template(template, GASTHAUSBOX_TEMPLATE_NAME, GASTHAUSBOX_DICT)
802
803
804 def gasthausbox_to_template(value):
805     return wikibox_to_template(value, GASTHAUSBOX_TEMPLATE_NAME, GASTHAUSBOX_DICT)
806
807
808 def gasthausbox_from_str(value):
809     """Returns an ordered dict."""
810     return wikibox_from_str(value, GASTHAUSBOX_TEMPLATE_NAME, GASTHAUSBOX_DICT)
811
812
813 def gasthausbox_to_str(value):
814     template = gasthausbox_to_template(value)
815     format_template_table(template, 17)
816     return str(template)
817
818
819 # Helper function to make page title pretty
820 # -----------------------------------------
821
822 def sledrun_page_title_to_pretty_url(page_title):
823     """Converts a page_title from the page_title column of wrsledruncache to name_url.
824     name_url is not used by MediaWiki but by new applications like wrweb."""
825     return page_title.lower().replace(' ', '-').replace('_', '-').replace('(', '').replace(')', '')