Coverage for src/local_deep_research/web_search_engines/engines/_google_pse_rate_limiter.py: 100%
35 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 15:42 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 15:42 +0000
1"""Process-wide minimum request spacing for Google PSE engine instances."""
3import secrets
4import threading
5import time
7_meta_lock = threading.Lock()
8_scope_locks: dict[str, threading.Lock] = {}
9_scope_last_request: dict[str, float] = {}
10_scope_nonce = secrets.token_urlsafe(32)
13def _scope_key(api_key: str, search_engine_id: str) -> str:
14 """Return a process-local opaque ID without retaining raw credentials.
16 Python string hashing is process-keyed SipHash. Two independently ordered
17 tuple hashes plus a random nonce give the in-memory limiter a compact scope
18 identity without treating API credentials as password-verification data.
19 """
20 mask = (1 << 64) - 1
21 first = hash((_scope_nonce, api_key, search_engine_id)) & mask
22 second = hash((search_engine_id, api_key, _scope_nonce)) & mask
23 return f"{first:016x}{second:016x}"
26def _get_scope_lock(scope: str) -> threading.Lock:
27 with _meta_lock:
28 lock = _scope_locks.get(scope)
29 if lock is None:
30 lock = threading.Lock()
31 _scope_locks[scope] = lock
32 return lock
35def respect_rate_limit(
36 api_key: str,
37 search_engine_id: str,
38 interval_seconds: float,
39) -> float:
40 """Sleep as needed to reserve a request slot for one PSE configuration."""
41 if interval_seconds <= 0:
42 return 0.0
44 scope = _scope_key(api_key, search_engine_id)
45 lock = _get_scope_lock(scope)
46 with lock:
47 elapsed = time.monotonic() - _scope_last_request.get(scope, 0.0)
48 wait_time = max(0.0, interval_seconds - elapsed)
49 if wait_time > 0:
50 time.sleep(wait_time)
51 _scope_last_request[scope] = time.monotonic()
52 return wait_time
55def reset_for_tests() -> None:
56 """Clear process-wide state. Intended for unit tests only."""
57 with _meta_lock:
58 _scope_locks.clear()
59 _scope_last_request.clear()