File size: 4,870 Bytes
e70c280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
from __future__ import annotations

import os
import subprocess
import sys
import time
from pathlib import Path

SENTENCE_TRANSFORMER_MODELS = (
    "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
    "intfloat/e5-small",
)

MAX_ATTEMPTS = 10
BASE_DELAY_SECONDS = 10
MAX_DELAY_SECONDS = 120


def _exception_chain(exc: BaseException):
    seen: set[int] = set()
    current: BaseException | None = exc
    while current is not None and id(current) not in seen:
        seen.add(id(current))
        yield current
        current = current.__cause__ or current.__context__


def _is_rate_limited(exc: BaseException) -> bool:
    for item in _exception_chain(exc):
        response = getattr(item, "response", None)
        if getattr(response, "status_code", None) == 429:
            return True
        text = str(item).lower()
        if (
            "429" in text
            or "too many requests" in text
            or "rate limit" in text
            or "ratelimit" in text
        ):
            return True
    return False


def _is_transient_hub_error(exc: BaseException) -> bool:
    if _is_rate_limited(exc):
        return True
    for item in _exception_chain(exc):
        text = str(item).lower()
        if (
            "couldn't connect to 'https://huggingface.co'" in text
            or "localentrynotfounderror" in type(item).__name__.lower()
            or "temporarily unavailable" in text
            or "service unavailable" in text
            or "gateway timeout" in text
        ):
            return True
    return False


def _retry_after_seconds(exc: BaseException) -> float | None:
    for item in _exception_chain(exc):
        response = getattr(item, "response", None)
        headers = getattr(response, "headers", None) or {}
        raw = headers.get("Retry-After") or headers.get("retry-after")
        if raw is None:
            continue
        try:
            return float(raw)
        except (TypeError, ValueError):
            continue
    return None


def _retry(action, label: str) -> None:
    for attempt in range(1, MAX_ATTEMPTS + 1):
        try:
            action()
            return
        except Exception as exc:
            if attempt == MAX_ATTEMPTS or not _is_transient_hub_error(exc):
                raise
            retry_after = _retry_after_seconds(exc)
            exponential = min(
                BASE_DELAY_SECONDS * (2 ** (attempt - 1)),
                MAX_DELAY_SECONDS,
            )
            delay = max(retry_after or 0.0, float(exponential))
            kind = "rate limited" if _is_rate_limited(exc) else "hub error"
            print(
                f"[preload] {label}: {kind}, "
                f"retry {attempt}/{MAX_ATTEMPTS} in {delay:.0f}s "
                f"({type(exc).__name__}: {exc})",
                flush=True,
            )
            time.sleep(delay)


def _configure_hf_token() -> None:
    token = (
        os.environ.get("HF_TOKEN")
        or os.environ.get("HUGGING_FACE_HUB_TOKEN")
        or ""
    ).strip()
    if not token:
        print(
            "[preload] warning: no HF_TOKEN/HUGGING_FACE_HUB_TOKEN set; "
            "anonymous Hugging Face downloads are frequently rate-limited in CI",
            flush=True,
        )
        return
    os.environ["HF_TOKEN"] = token
    os.environ["HUGGING_FACE_HUB_TOKEN"] = token
    print("[preload] Hugging Face token configured", flush=True)
    try:
        from huggingface_hub import login

        login(token=token, add_to_git_credential=False)
    except Exception as exc:
        print(f"[preload] huggingface_hub.login skipped: {exc}", flush=True)


def _preload_sentence_transformers() -> None:
    from sentence_transformers import SentenceTransformer

    for model in SENTENCE_TRANSFORMER_MODELS:
        print(f"[preload] loading sentence-transformers model: {model}", flush=True)
        _retry(lambda model=model: SentenceTransformer(model), model)


def _preload_spacy() -> None:
    root = Path(__file__).resolve().parents[1]
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))
    from src.constants.spacy_models import SPACY_MODEL_NAMES

    for model in sorted(set(SPACY_MODEL_NAMES.values())):

        def download(model_name: str = model) -> None:
            result = subprocess.run(
                [sys.executable, "-m", "spacy", "download", model_name],
                check=False,
            )
            if result.returncode != 0:
                raise RuntimeError(
                    f"spacy download {model_name} failed with exit code {result.returncode}"
                )

        print(f"[preload] downloading spaCy model: {model}", flush=True)
        _retry(download, model)


def main() -> None:
    _configure_hf_token()
    _preload_sentence_transformers()
    _preload_spacy()


if __name__ == "__main__":
    main()