quatuorbellefeuille.com

Content, build scripts and admin scripts for the Bellefeuille Quartet website.
git clone https://git.kevinlegouguec.net/quatuorbellefeuille.com
Log | Files | Refs

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 }