|
25 | 25 | import ssl |
26 | 26 | import subprocess |
27 | 27 | import sys |
| 28 | +import time |
28 | 29 | import urllib.error |
29 | 30 | import urllib.parse |
30 | 31 | import urllib.request |
31 | 32 | from datetime import datetime, timedelta, timezone |
32 | 33 | from pathlib import Path |
33 | 34 |
|
34 | 35 | API = "https://rest.api.transifex.com" |
| 36 | + |
| 37 | +# Wie oft ein voruebergehender Serverfehler wiederholt wird. |
| 38 | +VERSUCHE = 4 |
35 | 39 | KUERZUNG = 160 |
36 | 40 |
|
37 | 41 |
|
@@ -91,28 +95,49 @@ def ressourcen(wurzel: Path) -> tuple: |
91 | 95 | return karte, org, projekt |
92 | 96 |
|
93 | 97 |
|
94 | | -def seiten(pfad: str, params: dict, tok: str): |
95 | | - """Liefert (data-Eintrag, included-Karte) fuer alle Seiten.""" |
96 | | - url = API + pfad + ("?" + urllib.parse.urlencode(params) if params else "") |
97 | | - while url: |
| 98 | +def hole(url: str, tok: str) -> dict: |
| 99 | + """Eine Seite abrufen -- mit Wiederholung bei voruebergehenden Fehlern. |
| 100 | +
|
| 101 | + Transifex liefert sporadisch ein 502. Frueher wurde die Ressource dann |
| 102 | + stillschweigend uebersprungen und fehlte im Bericht. |
| 103 | + """ |
| 104 | + for versuch in range(1, VERSUCHE + 1): |
98 | 105 | req = urllib.request.Request(url, headers={ |
99 | | - "Authorization": "Bearer " + tok, |
| 106 | + "Authorization": f"Bearer {tok}", |
100 | 107 | "Accept": "application/vnd.api+json", |
101 | 108 | }) |
102 | 109 | try: |
103 | 110 | with urllib.request.urlopen(req, timeout=90, context=KONTEXT) as antwort: |
104 | | - daten = json.load(antwort) |
| 111 | + return json.load(antwort) |
105 | 112 | except urllib.error.HTTPError as e: |
| 113 | + if (e.code >= 500 or e.code == 429) and versuch < VERSUCHE: |
| 114 | + warte = 2 ** versuch |
| 115 | + print(f" HTTP {e.code}, neuer Versuch in {warte}s " |
| 116 | + f"({versuch}/{VERSUCHE - 1})", file=sys.stderr) |
| 117 | + time.sleep(warte) |
| 118 | + continue |
106 | 119 | rumpf = e.read().decode("utf-8", "replace")[:300] |
107 | 120 | raise RuntimeError(f"HTTP {e.code}: {rumpf}") from None |
108 | 121 | except urllib.error.URLError as e: |
109 | 122 | if "CERTIFICATE_VERIFY_FAILED" in str(e.reason): |
110 | 123 | sys.exit('Wurzelzertifikate fehlen -- "Install Certificates.command" ' |
111 | 124 | "ausfuehren oder certifi installieren.") |
112 | | - raise RuntimeError(f"Netzwerkfehler: {e.reason}") from None |
113 | | - beigefuegt = {e["id"]: e for e in daten.get("included", [])} |
| 125 | + if versuch < VERSUCHE: |
| 126 | + warte = 2 ** versuch |
| 127 | + print(f" Netzwerkfehler, neuer Versuch in {warte}s", file=sys.stderr) |
| 128 | + time.sleep(warte) |
| 129 | + continue |
| 130 | + raise RuntimeError(f"Netzwerkfehler bei {url}: {e.reason}") from None |
| 131 | + raise RuntimeError(f"Nach {VERSUCHE} Versuchen aufgegeben: {url}") |
| 132 | + |
| 133 | + |
| 134 | +def seiten(pfad: str, params: dict, tok: str): |
| 135 | + """Alle Seiten einer JSON:API-Sammlung durchlaufen.""" |
| 136 | + url = API + pfad + ("?" + urllib.parse.urlencode(params) if params else "") |
| 137 | + while url: |
| 138 | + daten = hole(url, tok) |
114 | 139 | for eintrag in daten.get("data", []): |
115 | | - yield eintrag, beigefuegt |
| 140 | + yield eintrag |
116 | 141 | url = (daten.get("links") or {}).get("next") |
117 | 142 |
|
118 | 143 |
|
|
0 commit comments