1212# * recreate_tx_config: recreate configuration for all resources.
1313# * warn_about_files_to_delete: lists files that are not available upstream
1414# * generate_commit_msg: generates commit message with co-authors
15+ # * fetch_glossary: download Polish terminology as a Hunspell personal dictionary
1516
1617from argparse import ArgumentParser
18+ import csv
19+ import io
1720import os
1821from contextlib import chdir
1922from dataclasses import dataclass
2023from pathlib import Path
24+ import re
2125from subprocess import call , run , CalledProcessError
2226import sys
2327from tempfile import TemporaryDirectory
28+ import time
2429from typing import Self , Iterable
30+ import unicodedata
2531from warnings import warn
2632
2733from polib import pofile , POFile
34+ import requests
2835from transifex .api import transifex_api
36+ from transifex .api .jsonapi .exceptions import JsonApiException
2937
3038LANGUAGE = 'pl'
3139PROJECT_SLUG = 'python-newest'
3240VERSION = '3.14'
41+ GLOSSARY_ID = 'o:python-doc:g:python_global'
3342
3443
3544def fetch ():
@@ -45,6 +54,59 @@ def fetch():
4554 _call (f'msgcat --no-location -o { file } { file } ' )
4655
4756
57+ def _download_glossary (token : str , * , max_wait : float = 300 ) -> str :
58+ transifex_api .setup (auth = token )
59+ # The SDK supports new API resource types before adding dedicated classes.
60+ download = transifex_api .new (
61+ type = 'glossaries_async_downloads' ,
62+ glossary = transifex_api .new (type = 'glossaries' , id = GLOSSARY_ID ),
63+ language = transifex_api .Language (id = f'l:{ LANGUAGE } ' ),
64+ )
65+ download .save ()
66+ deadline = time .monotonic () + max_wait
67+ while not download .redirect :
68+ status = download .attributes ['status' ]
69+ if status == 'failed' :
70+ raise ValueError ('Transifex glossary export failed.' )
71+ if status not in {'pending' , 'processing' }:
72+ raise ValueError (f'Unexpected glossary export status: { status !r} ' )
73+ if time .monotonic () >= deadline :
74+ raise ValueError ('Transifex glossary export timed out.' )
75+ time .sleep (3 )
76+ download .reload ()
77+ # Download the CSV without sending the API token to the signed storage URL.
78+ export = requests .get (download .redirect , timeout = 30 )
79+ export .raise_for_status ()
80+ return export .content .decode ('utf-8-sig' )
81+
82+
83+ def _glossary_words (content : str ) -> list [str ]:
84+ reader = csv .DictReader (io .StringIO (content ))
85+ translation_column = f'translation_{ LANGUAGE } '
86+ if not reader .fieldnames or translation_column not in reader .fieldnames :
87+ raise ValueError ('Transifex glossary CSV has no Polish translation column.' )
88+ words = {
89+ word
90+ for row in reader
91+ for word in re .findall (
92+ r'[^\W\d_]+' , unicodedata .normalize ('NFC' , row [translation_column ] or '' )
93+ )
94+ }
95+ if not words :
96+ raise ValueError ('Transifex glossary has no Polish terms.' )
97+ return sorted (words )
98+
99+
100+ def fetch_glossary (output : Path ):
101+ """Fetch Polish terminology and write a UTF-8 Hunspell personal dictionary."""
102+ token = _get_tx_token ().strip ()
103+ if not token :
104+ raise ValueError ('TX_TOKEN is required to download the Transifex glossary.' )
105+ words = _glossary_words (_download_glossary (token ))
106+ output .write_text (f'{ len (words )} \n ' + '\n ' .join (words ) + '\n ' , encoding = 'utf-8' )
107+ print (f'Added { len (words )} Polish glossary words to { output } .' )
108+
109+
48110def _call (command : str ):
49111 if (return_code := call (command , shell = True )) != 0 :
50112 exit (return_code )
@@ -223,10 +285,30 @@ def generate_commit_msg():
223285 'recreate_tx_config' ,
224286 'warn_about_files_to_delete' ,
225287 'generate_commit_msg' ,
288+ 'fetch_glossary' ,
226289 )
227290
228291 parser = ArgumentParser ()
229292 parser .add_argument ('cmd' , choices = RUNNABLE_SCRIPTS )
293+ parser .add_argument (
294+ '--output' , type = Path , help = 'Hunspell dictionary path for fetch_glossary'
295+ )
230296 options = parser .parse_args ()
231297
232- eval (options .cmd )()
298+ if options .cmd == 'fetch_glossary' :
299+ if options .output is None :
300+ parser .error ('--output is required for fetch_glossary' )
301+ try :
302+ fetch_glossary (options .output )
303+ except (requests .RequestException , JsonApiException ) as error :
304+ # Exceptions may contain signed URLs; report the status without those URLs.
305+ status = (
306+ error .response .status_code
307+ if error .response is not None
308+ else 'network error'
309+ )
310+ parser .exit (1 , f'Transifex glossary request failed: { status } .\n ' )
311+ except (ValueError , KeyError ) as error :
312+ parser .exit (1 , f'Invalid Transifex glossary export: { error } \n ' )
313+ else :
314+ eval (options .cmd )()
0 commit comments