Skip to content

Commit 632fe03

Browse files
committed
http client is now a separate module, refactor and update tests
1 parent cd57747 commit 632fe03

6 files changed

Lines changed: 58 additions & 53 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""HTTP client factory for Bubble Data API."""
2+
3+
import httpx
4+
5+
DEFAULT_USER_AGENT = "bubble-data-api-client"
6+
DEFAULT_TIMEOUT_SECONDS = 60.0
7+
DEFAULT_RETRY_COUNT = 3
8+
9+
10+
def httpx_client_factory(
11+
*,
12+
base_url: str,
13+
api_key: str,
14+
user_agent: str = DEFAULT_USER_AGENT,
15+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
16+
retries: int = DEFAULT_RETRY_COUNT,
17+
) -> httpx.AsyncClient:
18+
"""Create a configured async HTTP client for the Bubble Data API."""
19+
return httpx.AsyncClient(
20+
base_url=base_url,
21+
headers={
22+
"Authorization": f"Bearer {api_key}",
23+
"User-Agent": user_agent,
24+
},
25+
transport=httpx.AsyncHTTPTransport(retries=retries),
26+
timeout=httpx.Timeout(timeout),
27+
)

src/bubble_data_api_client/pool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from bubble_data_api_client.config import get_config
1212
from bubble_data_api_client.exceptions import ConfigurationError
13-
from bubble_data_api_client.transport import httpx_client_factory
13+
from bubble_data_api_client.http_client import httpx_client_factory
1414

1515
# global client pool keyed by config
1616
_clients: dict[tuple[str, str], httpx.AsyncClient] = {}

src/bubble_data_api_client/transport.py

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,23 @@
1+
"""HTTP transport layer for Bubble Data API requests."""
2+
13
import types
24
import typing
35

46
import httpx
57

6-
DEFAULT_USER_AGENT = "bubble-data-api-client"
8+
from bubble_data_api_client.pool import get_client
79

810

9-
def httpx_client_factory(
10-
base_url: str,
11-
api_key: str,
12-
) -> httpx.AsyncClient:
13-
return httpx.AsyncClient(
14-
base_url=base_url,
15-
headers={
16-
"Authorization": f"Bearer {api_key}",
17-
"User-Agent": DEFAULT_USER_AGENT,
18-
},
19-
transport=httpx.AsyncHTTPTransport(retries=3),
20-
timeout=httpx.Timeout(60.0),
21-
)
11+
class Transport:
12+
"""Async context manager for HTTP operations.
2213
14+
Responsibilities:
15+
- Obtains a pooled httpx client on entry
16+
- Provides HTTP verb methods (get, post, patch, put, delete)
17+
- Raises on non-2xx responses
2318
24-
class Transport:
25-
"""
26-
Transport layer focuses on HTTP.
27-
- authentication, headers, retries, timeouts: configured via httpx_client_factory
28-
- connection lifecycle: managed by pool module
29-
- HTTP verb methods: get, post, patch, put, delete
30-
- error handling: raise_for_status on responses
19+
HTTP client configuration (headers, retries, timeouts) is handled by
20+
the http_client module. Connection pooling is handled by the pool module.
3121
"""
3222

3323
_http: httpx.AsyncClient
@@ -36,9 +26,6 @@ def __init__(self) -> None:
3626
pass
3727

3828
async def __aenter__(self) -> typing.Self:
39-
# deferred import to avoid circular dependency: pool imports transport
40-
from bubble_data_api_client.pool import get_client
41-
4229
self._http = get_client()
4330
return self
4431

src/tests/conftest.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from bubble_data_api_client import configure, settings # noqa: E402
1111
from bubble_data_api_client.client import raw_client # noqa: E402
12+
from bubble_data_api_client.pool import close_clients # noqa: E402
1213

1314

1415
@pytest.fixture
@@ -22,7 +23,7 @@ def test_api_key() -> str:
2223

2324

2425
@pytest.fixture(autouse=True)
25-
def auto_configure_client():
26+
async def auto_configure_client() -> AsyncGenerator[None]:
2627
"""Automatically configure the client for every test run."""
2728
if not settings.BUBBLE_DATA_API_ROOT_URL:
2829
raise RuntimeError("BUBBLE_DATA_API_ROOT_URL")
@@ -34,19 +35,15 @@ def auto_configure_client():
3435
api_key=settings.BUBBLE_API_KEY,
3536
)
3637

38+
yield
39+
40+
await close_clients()
41+
3742

3843
@pytest.fixture
3944
async def bubble_raw_client() -> AsyncGenerator[raw_client.RawClient]:
4045
"""Provide a raw client for testing the low-level API."""
41-
if not settings.BUBBLE_DATA_API_ROOT_URL:
42-
raise RuntimeError("BUBBLE_DATA_API_ROOT_URL")
43-
if not settings.BUBBLE_API_KEY:
44-
raise RuntimeError("BUBBLE_API_KEY")
45-
46-
async with raw_client.RawClient(
47-
data_api_root_url=settings.BUBBLE_DATA_API_ROOT_URL,
48-
api_key=settings.BUBBLE_API_KEY,
49-
) as client_instance:
46+
async with raw_client.RawClient() as client_instance:
5047
yield client_instance
5148

5249

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
11
from bubble_data_api_client.client import raw_client
22

33

4-
async def test_raw_client_init(test_url: str, test_api_key: str):
5-
"""Test that client is instantiated."""
6-
4+
async def test_raw_client_init() -> None:
5+
"""Test that RawClient can be instantiated and used as context manager."""
76
# test creating an instance
8-
client = raw_client.RawClient(
9-
data_api_root_url=test_url,
10-
api_key=test_api_key,
11-
)
7+
client = raw_client.RawClient()
128
assert isinstance(client, raw_client.RawClient)
139

1410
# test async context manager
1511
async with client as client_instance:
1612
assert isinstance(client_instance, raw_client.RawClient)
1713

1814
# test creating with async context manager
19-
async with raw_client.RawClient(
20-
data_api_root_url=test_url,
21-
api_key=test_api_key,
22-
) as client_instance:
15+
async with raw_client.RawClient() as client_instance:
2316
assert isinstance(client_instance, raw_client.RawClient)

src/tests/unit/test_transport.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import httpx
22

3-
from bubble_data_api_client import transport
3+
from bubble_data_api_client import http_client
44

55

6-
def test_httpx_client_factory(test_url: str, test_api_key: str):
7-
"""Test that http client is instantiated."""
8-
httpx_client = transport.httpx_client_factory(
6+
def test_httpx_client_factory(test_url: str, test_api_key: str) -> None:
7+
"""Test that HTTP client is instantiated with correct configuration."""
8+
client = http_client.httpx_client_factory(
99
base_url=test_url,
1010
api_key=test_api_key,
1111
)
12-
assert isinstance(httpx_client, httpx.AsyncClient)
13-
assert httpx_client.base_url == test_url
14-
assert httpx_client.headers["Authorization"] == f"Bearer {test_api_key}"
12+
assert isinstance(client, httpx.AsyncClient)
13+
assert client.base_url == test_url
14+
assert client.headers["Authorization"] == f"Bearer {test_api_key}"
15+
assert client.headers["User-Agent"] == http_client.DEFAULT_USER_AGENT

0 commit comments

Comments
 (0)