helpers.py (4832B)
1 from contextlib import contextmanager 2 from dataclasses import dataclass 3 from datetime import datetime 4 import locale 5 from operator import attrgetter 6 from os import path 7 from pathlib import Path 8 import re 9 from typing import Iterator, Optional 10 11 12 def guess_language(filename): 13 parent = str(Path(filename).parent) 14 if parent == '.': 15 return 'fr' 16 return parent 17 18 19 def relative_path(*, to, ref): 20 # pathlib.Path(x).relative_to(y) cannot handle y not being under x, 21 # os.path.dirname('x') yields '' rather than '.'. 22 # 😮💨 23 return path.relpath(to, Path(ref).parent) 24 25 26 @contextmanager 27 def tmplocale(lang): 28 old_lang, encoding = locale.getlocale() 29 try: 30 locale.setlocale(locale.LC_TIME, (lang, encoding)) 31 yield 32 finally: 33 locale.setlocale(locale.LC_TIME, (old_lang, encoding)) 34 35 36 _LICENSE_URLS = { 37 'CC0': 'https://creativecommons.org/publicdomain/zero', 38 'CC BY': 'https://creativecommons.org/licenses/by', 39 'CC BY-SA': 'https://creativecommons.org/licenses/by-sa', 40 } 41 42 _LICENSE_RE = re.compile( 43 '('+'|'.join(_LICENSE_URLS.keys())+')' + ' ([0-9.]+)' 44 ) 45 46 47 @dataclass 48 class LicenseInfo: 49 tag: str 50 version: str 51 52 @classmethod 53 def deserialize(cls, info): 54 if info is None: 55 return None 56 return cls(*_LICENSE_RE.fullmatch(info).groups()) 57 58 def format(self): 59 url = f'{_LICENSE_URLS[self.tag]}/{self.version}/' 60 61 return f'<a href="{url}" target="_blank">{self.tag}</a>' 62 63 64 @dataclass 65 class Illustration: 66 file: str 67 alt_text: str 68 source_name: str 69 source_link: Optional[str] 70 license_info: Optional[LicenseInfo] 71 position: Optional[str] 72 73 @classmethod 74 def deserialize(cls, d): 75 return cls(d['pic_file'], 76 d['pic_alt'], 77 d['pic_src'], 78 d['pic_link'], 79 LicenseInfo.deserialize(d['pic_license']), 80 d['pic_position']) 81 82 @property 83 def style(self): 84 if self.position is None: 85 return None 86 87 return f'object-position: {self.position}' 88 89 90 @dataclass 91 class Concert: 92 time: datetime 93 place: str 94 address: str 95 pieces: Iterator[str] 96 instructions: str 97 illustration: Illustration 98 warning: Optional[str] 99 100 @classmethod 101 def deserialize(cls, d): 102 return cls( 103 time=datetime.strptime(d['time'], '%d/%m/%Y %Hh%M'), 104 place=d['place'], 105 address=d['address'], 106 pieces=d['pieces'], 107 instructions=d['instructions'], 108 illustration=Illustration.deserialize(d), 109 warning=d['warning'] 110 ) 111 112 113 def _optional(line): 114 return f'(?:{line})?' 115 116 117 _CONCERT_LINES = ( 118 r'QUAND : (?P<time>[^\n]+)\n', 119 r'O[UÙ] : (?P<place>[^\n]+)\n', 120 'ADRESSE :\n', 121 '(?P<address>.+?)\n', 122 'PROGRAMME :\n', 123 '(?P<pieces>.+?)\n', 124 'INSTRUCTIONS :\n', 125 '(?P<instructions>.+?)\n', 126 'ILLUSTRATION :\n', 127 r'fichier : (?P<pic_file>[^\n]+)\n', 128 r'légende : (?P<pic_alt>[^\n]+)\n', 129 r'source : (?P<pic_src>[^\n]+)\n', 130 _optional(r'lien : (?P<pic_link>[^\n]+)\n'), 131 _optional(r'licence : (?P<pic_license>[^\n]+)\n'), 132 _optional(r'position : (?P<pic_position>[^\n]+)\n'), 133 _optional(r'AVERTISSEMENT : (?P<warning>[^\n]+)\n'), 134 ) 135 136 _CONCERT_RE = re.compile(''.join(_CONCERT_LINES), flags=re.DOTALL) 137 138 139 def read_concerts(filename): 140 with open(filename) as f: 141 concerts = ( 142 Concert.deserialize(match) 143 for match in re.finditer(_CONCERT_RE, f.read()) 144 ) 145 return tuple(sorted(concerts, key=attrgetter('time'))) 146 147 148 def split_concerts(concerts, threshold): 149 cutoff = len(concerts) 150 151 for i, c in enumerate(concerts): 152 if c.time > threshold: 153 cutoff = i 154 break 155 156 return concerts[:cutoff], concerts[cutoff:] 157 158 159 _TOUCHUPS = ( 160 # TODO: extend to other ordinals. 161 # Cf. <https://fr.wikipedia.org/wiki/Adjectif_numéral_en_français#Abréviation_des_ordinaux> 162 (re.compile(r'([0-9])(st|nd|rd|th|er|ère|e)\b'), r'\1<sup>\2</sup>'), 163 (re.compile('<(https?://[^ ]+)>'), r'<a href="\1" target="_blank">\1</a>'), 164 (re.compile(r'\[([^]]+)\]\((https?://[^ ]+)\)'), r'<a href="\2" target="_blank">\1</a>'), 165 (re.compile(r'\[([^]]+)\]\((tel:[+\d]+)\)'), r'<a href="\2">\1</a>'), 166 (re.compile('([^ ]+@[^ ]+)'), r'<a href="mailto:\1">\1</a>'), 167 ) 168 169 170 def touchup_plaintext(plaintext): 171 text = plaintext 172 for regexp, repl in _TOUCHUPS: 173 text = regexp.sub(repl, text) 174 return text 175 176 177 DATE_FORMATTERS = { 178 'en': { 179 'date': lambda d: d.strftime('%A %B %-d, %Y'), 180 'time': lambda d: d.strftime('%I:%M %P'), 181 }, 182 'fr': { 183 'date': lambda d: d.strftime('%A %-d %B %Y').capitalize(), 184 'time': lambda d: d.strftime('%Hh%M'), 185 }, 186 }