File size: 2,226 Bytes
8b96826
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
"""HTTP helpers: secret scrubbing, friendly error text, retry-on-429.

Every error string that can reach a log line or the browser must pass
through scrub_secrets()/friendly_error() β€” requests includes the full
request URL (with ?key=...) in HTTPError messages.
"""

import re
import time

import requests

_SECRET_PATTERN = re.compile(r"((?:api[_-]?key|apikey|key)=)[^&\s]+", re.IGNORECASE)


def scrub_secrets(text):
    """Mask API keys in URLs/error messages: key=abc123 -> key=***"""
    return _SECRET_PATTERN.sub(r"\1***", str(text))


def friendly_error(error):
    """Short, key-free, human-readable message for the UI."""
    text = str(error)
    if "429" in text:
        return "Traffic API rate limit hit β€” wait ~30 seconds and try again."
    if "401" in text or "403" in text:
        return "A data API rejected its key β€” check the keys in .env."
    if "timeout" in text.lower() or "timed out" in text.lower():
        return "Network timeout while calling a data API β€” try again."
    return scrub_secrets(text)


def get_with_retry(url, params=None, timeout=20, retries=3, backoff=1.5):
    """GET with automatic retry on 429/5xx. Other 4xx raise immediately."""
    last_error = None
    for attempt in range(retries):
        try:
            response = requests.get(url, params=params, timeout=timeout)
            if response.status_code == 429:
                last_error = requests.HTTPError(
                    f"429 Too Many Requests (attempt {attempt + 1}/{retries})",
                    response=response,
                )
                time.sleep(backoff * (attempt + 1))
                continue
            response.raise_for_status()
            return response
        except requests.HTTPError as error:
            status = getattr(error.response, "status_code", None)
            if status is not None and 400 <= status < 500 and status != 429:
                raise  # genuine client error β€” retrying won't help
            last_error = error
            time.sleep(backoff * (attempt + 1))
        except requests.RequestException as error:  # timeouts, connection drops
            last_error = error
            time.sleep(backoff * (attempt + 1))
    raise last_error