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