Skip to content

Commit 6ae5e2b

Browse files
author
Lucas Alencar Xisto
committed
test: dedupe retries/timeouts, add conftest with fake OPENAI_API_KEY
1 parent 9aedefd commit 6ae5e2b

File tree

7 files changed

+151
-50
lines changed

7 files changed

+151
-50
lines changed

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@
2121
logging.getLogger("openai").setLevel(logging.DEBUG)
2222

2323

24+
# Autouse fixture to ensure an API key is always set for tests
25+
@pytest.fixture(autouse=True)
26+
def _fake_openai_key(monkeypatch: pytest.MonkeyPatch) -> None:
27+
# evita dependência real de credencial
28+
monkeypatch.setenv("OPENAI_API_KEY", "test")
29+
yield
30+
31+
2432
# automatically add `pytest.mark.asyncio()` to all of our async tests
2533
# so we don't have to add that boilerplate everywhere
2634
def pytest_collection_modifyitems(items: list[pytest.Function]) -> None:

tests/retries/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""Tests related to retry behavior."""
2+

tests/retries/test_retry_after.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from unittest import mock
5+
6+
import httpx
7+
import pytest
8+
from respx import MockRouter
9+
10+
from openai import OpenAI
11+
12+
13+
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
14+
15+
16+
def _low_retry_timeout(*_args, **_kwargs) -> float:
17+
return 0.01
18+
19+
20+
@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
21+
@pytest.mark.respx(base_url=base_url)
22+
def test_retry_after_header_is_respected(respx_mock: MockRouter, client: OpenAI) -> None:
23+
attempts = {"n": 0}
24+
25+
def handler(request: httpx.Request) -> httpx.Response:
26+
attempts["n"] += 1
27+
if attempts["n"] == 1:
28+
return httpx.Response(429, headers={"Retry-After": "2"}, json={"err": "rate"})
29+
return httpx.Response(200, json={"ok": True})
30+
31+
respx_mock.post("/chat/completions").mock(side_effect=handler)
32+
33+
client = client.with_options(max_retries=3)
34+
35+
response = client.chat.completions.with_raw_response.create(
36+
messages=[{"content": "hi", "role": "user"}],
37+
model="gpt-4o",
38+
)
39+
40+
assert response.retries_taken == 1
41+
assert int(response.http_request.headers.get("x-stainless-retry-count")) == 1
42+
Lines changed: 50 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,50 @@
1-
import httpx
2-
import pytest
3-
from openai import AsyncOpenAI, DefaultAsyncHttpxClient
4-
5-
@pytest.mark.anyio
6-
async def test_images_generate_includes_content_filter_results_async():
7-
"""
8-
Ensure the Image model exposes optional fields returned by the API,
9-
specifically `content_filter_results` (keeping `revised_prompt` coverage).
10-
"""
11-
mock_json = {
12-
"created": 1711111111,
13-
"data": [
14-
{
15-
"url": "https://example.test/cat.png",
16-
"revised_prompt": "a cute cat wearing sunglasses",
17-
"content_filter_results": {
18-
"sexual_minors": {"filtered": False},
19-
"violence": {"filtered": False},
20-
},
21-
}
22-
],
23-
}
24-
25-
# Async handler because we'll use AsyncOpenAI (httpx.AsyncClient under the hood)
26-
async def ahandler(request: httpx.Request) -> httpx.Response:
27-
assert "images" in str(request.url).lower()
28-
return httpx.Response(200, json=mock_json)
29-
30-
atransport = httpx.MockTransport(ahandler)
31-
32-
client = AsyncOpenAI(
33-
api_key="test",
34-
http_client=DefaultAsyncHttpxClient(transport=atransport),
35-
timeout=10.0,
36-
)
37-
38-
resp = await client.images.generate(model="gpt-image-1", prompt="cat with glasses") # type: ignore
39-
40-
assert hasattr(resp, "data") and isinstance(resp.data, list) and resp.data
41-
item = resp.data[0]
42-
43-
# existing field
44-
assert item.revised_prompt == "a cute cat wearing sunglasses"
45-
46-
# new optional field
47-
cfr = item.content_filter_results
48-
assert isinstance(cfr, dict), f"content_filter_results should be dict, got {type(cfr)}"
49-
assert cfr.get("violence", {}).get("filtered") is False
50-
assert cfr.get("sexual_minors", {}).get("filtered") is False
1+
import httpx
2+
import pytest
3+
from openai import AsyncOpenAI, DefaultAsyncHttpxClient
4+
5+
@pytest.mark.anyio
6+
async def test_images_generate_includes_content_filter_results_async():
7+
"""
8+
Ensure the Image model exposes optional fields returned by the API,
9+
specifically `content_filter_results` (keeping `revised_prompt` coverage).
10+
"""
11+
mock_json = {
12+
"created": 1711111111,
13+
"data": [
14+
{
15+
"url": "https://example.test/cat.png",
16+
"revised_prompt": "a cute cat wearing sunglasses",
17+
"content_filter_results": {
18+
"sexual_minors": {"filtered": False},
19+
"violence": {"filtered": False},
20+
},
21+
}
22+
],
23+
}
24+
25+
# Async handler because we'll use AsyncOpenAI (httpx.AsyncClient under the hood)
26+
async def ahandler(request: httpx.Request) -> httpx.Response:
27+
assert "images" in str(request.url).lower()
28+
return httpx.Response(200, json=mock_json)
29+
30+
atransport = httpx.MockTransport(ahandler)
31+
32+
client = AsyncOpenAI(
33+
api_key="test",
34+
http_client=DefaultAsyncHttpxClient(transport=atransport),
35+
timeout=10.0,
36+
)
37+
38+
resp = await client.images.generate(model="gpt-image-1", prompt="cat with glasses") # type: ignore
39+
40+
assert hasattr(resp, "data") and isinstance(resp.data, list) and resp.data
41+
item = resp.data[0]
42+
43+
# existing field
44+
assert item.revised_prompt == "a cute cat wearing sunglasses"
45+
46+
# new optional field
47+
cfr = item.content_filter_results
48+
assert isinstance(cfr, dict), f"content_filter_results should be dict, got {type(cfr)}"
49+
assert cfr.get("violence", {}).get("filtered") is False
50+
assert cfr.get("sexual_minors", {}).get("filtered") is False

tests/timeouts/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""Tests related to timeout behavior."""
2+

tests/timeouts/_util.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from __future__ import annotations
2+
3+
def assert_timeout_eq(value, expected: float) -> None:
4+
"""Assert that a timeout-like value equals the expected seconds.
5+
6+
Supports plain numeric timeouts or httpx.Timeout instances.
7+
"""
8+
from httpx import Timeout
9+
10+
if isinstance(value, (int, float)):
11+
assert float(value) == expected
12+
elif isinstance(value, Timeout):
13+
assert any(
14+
getattr(value, f, None) in (None, expected)
15+
for f in ("read", "connect", "write")
16+
), f"Timeout fields do not match {expected}: {value!r}"
17+
else:
18+
raise AssertionError(f"Unexpected timeout type: {type(value)}")
19+

tests/timeouts/test_overrides.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from __future__ import annotations
2+
3+
import os
4+
5+
import httpx
6+
import pytest
7+
8+
from openai import OpenAI
9+
from openai._models import FinalRequestOptions
10+
from openai._base_client import DEFAULT_TIMEOUT
11+
12+
13+
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
14+
15+
16+
def test_per_request_timeout_overrides_default(client: OpenAI) -> None:
17+
# default timeout applied when none provided per-request
18+
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
19+
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore[arg-type]
20+
assert timeout == DEFAULT_TIMEOUT
21+
22+
# per-request timeout overrides the default
23+
request = client._build_request(
24+
FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
25+
)
26+
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore[arg-type]
27+
assert timeout == httpx.Timeout(100.0)
28+

0 commit comments

Comments
 (0)