"""Offline tests for the retry/backoff layer. No network, no real sleeping.""" import pytest import requests import bls_client.client as client_mod from bls_client import BLSClient from bls_client.retry import with_retries class Resp: def __init__(self, status_code=200, headers=None): self.status_code = status_code self.headers = headers or {} def raise_for_status(self): pass def json(self): return { "status": "REQUEST_SUCCEEDED", "Results": {"series": [{"seriesID": "X", "data": [{"year": "2024", "period": "M01", "value": "1"}]}]}, } def _recorder(): delays = [] return delays, (lambda d: delays.append(d)) def test_retries_transient_exception_then_succeeds(): calls = {"n": 0} delays, sleep = _recorder() def send(): calls["n"] += 1 if calls["n"] < 3: raise requests.exceptions.ConnectionError("boom") return Resp(200) resp = with_retries(send, retries=3, backoff=0.5, sleep=sleep) assert resp.status_code == 200 assert calls["n"] == 3 assert delays == [0.5, 1.0] # exponential backoff between the 3 attempts def test_gives_up_after_max_retries(): delays, sleep = _recorder() def send(): raise requests.exceptions.Timeout("slow") with pytest.raises(requests.exceptions.Timeout): with_retries(send, retries=2, backoff=0.5, sleep=sleep) assert len(delays) == 2 # retried twice, then re-raised def test_retries_5xx_but_not_4xx(): delays, sleep = _recorder() seq = [Resp(503), Resp(200)] resp = with_retries(lambda: seq.pop(0), retries=3, backoff=0.5, sleep=sleep) assert resp.status_code == 200 and len(delays) == 1 delays2, sleep2 = _recorder() resp2 = with_retries(lambda: Resp(404), retries=3, backoff=0.5, sleep=sleep2) assert resp2.status_code == 404 and delays2 == [] # client error: not retried def test_honors_retry_after_header(): delays, sleep = _recorder() seq = [Resp(429, headers={"Retry-After": "7"}), Resp(200)] with_retries(lambda: seq.pop(0), retries=2, backoff=0.5, sleep=sleep) assert delays == [7.0] def test_client_retries_then_succeeds(monkeypatch): # no real sleeping monkeypatch.setattr("bls_client.retry.time.sleep", lambda d: None) state = {"n": 0} def fake_post(url, json=None, timeout=None): state["n"] += 1 if state["n"] == 1: raise requests.exceptions.ConnectionError("transient") return Resp(200) monkeypatch.setattr(client_mod.requests, "post", fake_post) c = BLSClient("key", retries=2) out = c.fetch(["CES0000000001"], 2024, 2024) assert out and state["n"] == 2 # failed once, retried, succeeded assert c.queries_used == 1 # counter only counts the successful POST def test_retries_zero_disables(monkeypatch): monkeypatch.setattr("bls_client.retry.time.sleep", lambda d: None) def fake_post(url, json=None, timeout=None): raise requests.exceptions.ConnectionError("transient") monkeypatch.setattr(client_mod.requests, "post", fake_post) with pytest.raises(requests.exceptions.ConnectionError): BLSClient("key", retries=0).fetch(["CES0000000001"], 2024, 2024)