Skip to content

Commit 989a60e

Browse files
authored
✨ Feature: add httpx transport config to client (#247)
1 parent 18ed668 commit 989a60e

File tree

6 files changed

+154
-3
lines changed

6 files changed

+154
-3
lines changed

docs/usage/configuration.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,23 @@ If `trust_env` is set to `True`, githubkit (httpx) will look for the environment
8484

8585
If you want to set a proxy for client programmatically, you can pass a proxy URL to the `proxy` option. See [httpx's proxies documentation](https://www.python-httpx.org/advanced/proxies/) for more information.
8686

87+
### `transport`, `async_transport`
88+
89+
These two options let you provide a custom [HTTPX transport](https://www.python-httpx.org/advanced/transports/) for the underlying HTTP client.
90+
91+
They accept instances of the following types:
92+
93+
- `httpx.BaseTransport` (sync transport) — pass via the `transport` option.
94+
- `httpx.AsyncBaseTransport` (async transport) — pass via the `async_transport` option.
95+
96+
When provided, githubkit will forward the transport to create the client. This is useful for:
97+
98+
- providing a custom network implementation;
99+
- injecting test-only transports (for example `httpx.MockTransport`) to stub responses in unit tests;
100+
- using alternative transports provided by HTTPX or third parties.
101+
102+
Note that if you pass `None` to the option, the default transport will be created by HTTPX.
103+
87104
### `cache_strategy`
88105

89106
The `cache_strategy` option defines how to cache the tokens or http responses. You can provide a githubkit built-in cache strategy or a custom one that implements the `BaseCacheStrategy` interface. By default, githubkit uses the `MemCacheStrategy` to cache the data in memory.

docs/usage/unit-test.md

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Unit Test
22

3-
If you are using githubkit in your business logic, you may want to mock the github API in your unit tests. You can custom the response by mocking the `request`/`arequest` method of the `GitHub` class. Here is an example of how to mock githubkit's API calls:
3+
If you are using githubkit in your business logic, you may want to mock the github API in your unit tests. There are two ways to reach this.
4+
5+
## Mocking the API Calls
6+
7+
If you can't provide a githubkit test client to your business logic, you can mock the `request`/`arequest` method of the `GitHub` class to custom the response. Here is an example of how to mock githubkit's API calls:
48

59
=== "Sync"
610

@@ -106,3 +110,66 @@ If you are using githubkit in your business logic, you may want to mock the gith
106110
1. Example function you want to test, which calls the GitHub API.
107111
2. other request parameters including headers, json, etc.
108112
3. When the request is made, return a fake response
113+
114+
## Using a Test Transport
115+
116+
You can also create a test client with mock transport and provide it to your business logic. Here is an example:
117+
118+
=== "Sync"
119+
120+
```python
121+
import json
122+
from pathlib import Path
123+
124+
import httpx
125+
import pytest
126+
127+
from githubkit import GitHub
128+
from githubkit.versions.latest.models import FullRepository
129+
130+
FAKE_RESPONSE = json.loads(Path("fake_response.json").read_text())
131+
132+
def target_sync_func(github: GitHub):
133+
resp = github.rest.repos.get("owner", "repo")
134+
return resp.parsed_data
135+
136+
def mock_transport_handler(request: httpx.Request) -> httpx.Response:
137+
if request.method == "GET" and request.url.path == "/repos/owner/repo":
138+
return httpx.Response(status_code=200, json=FAKE_RESPONSE)
139+
raise RuntimeError(f"Unexpected request: {request.method} {request.url.path}")
140+
141+
def test_sync_mock():
142+
g = GitHub("xxxxx", transport=httpx.MockTransport(mock_transport_handler))
143+
repo = target_sync_func(g)
144+
assert isinstance(repo, FullRepository)
145+
```
146+
147+
=== "Async"
148+
149+
```python
150+
import json
151+
from pathlib import Path
152+
153+
import httpx
154+
import pytest
155+
156+
from githubkit import GitHub
157+
from githubkit.versions.latest.models import FullRepository
158+
159+
FAKE_RESPONSE = json.loads(Path("fake_response.json").read_text())
160+
161+
async def target_async_func(github: GitHub):
162+
resp = await github.rest.repos.async_get("owner", "repo")
163+
return resp.parsed_data
164+
165+
def mock_transport_handler(request: httpx.Request) -> httpx.Response:
166+
if request.method == "GET" and request.url.path == "/repos/owner/repo":
167+
return httpx.Response(status_code=200, json=FAKE_RESPONSE)
168+
raise RuntimeError(f"Unexpected request: {request.method} {request.url.path}")
169+
170+
@pytest.mark.anyio
171+
async def test_async_mock():
172+
g = GitHub("xxxxx", async_transport=httpx.MockTransport(mock_transport_handler))
173+
repo = await target_async_func(g)
174+
assert isinstance(repo, FullRepository)
175+
```

githubkit/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class Config:
2424
ssl_verify: Union[bool, "ssl.SSLContext"]
2525
trust_env: bool # effects the `httpx` proxy and ssl cert
2626
proxy: Optional[ProxyTypes]
27+
transport: Optional[httpx.BaseTransport]
28+
async_transport: Optional[httpx.AsyncBaseTransport]
2729
cache_strategy: BaseCacheStrategy
2830
http_cache: bool
2931
throttler: BaseThrottler
@@ -113,6 +115,8 @@ def get_config(
113115
ssl_verify: Union[bool, "ssl.SSLContext"] = True,
114116
trust_env: bool = True,
115117
proxy: Optional[ProxyTypes] = None,
118+
transport: Optional[httpx.BaseTransport] = None,
119+
async_transport: Optional[httpx.AsyncBaseTransport] = None,
116120
cache_strategy: Optional[BaseCacheStrategy] = None,
117121
http_cache: bool = True,
118122
throttler: Optional[BaseThrottler] = None,
@@ -129,6 +133,8 @@ def get_config(
129133
ssl_verify,
130134
trust_env,
131135
proxy,
136+
transport,
137+
async_transport,
132138
build_cache_strategy(cache_strategy),
133139
http_cache,
134140
build_throttler(throttler),

githubkit/core.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ def __init__(
8888
ssl_verify: Union[bool, "ssl.SSLContext"] = ...,
8989
trust_env: bool = True,
9090
proxy: Optional[ProxyTypes] = None,
91+
transport: Optional[httpx.BaseTransport] = None,
92+
async_transport: Optional[httpx.AsyncBaseTransport] = None,
9193
cache_strategy: Optional[BaseCacheStrategy] = None,
9294
http_cache: bool = True,
9395
throttler: Optional[BaseThrottler] = None,
@@ -110,6 +112,8 @@ def __init__(
110112
ssl_verify: Union[bool, "ssl.SSLContext"] = ...,
111113
trust_env: bool = True,
112114
proxy: Optional[ProxyTypes] = None,
115+
transport: Optional[httpx.BaseTransport] = None,
116+
async_transport: Optional[httpx.AsyncBaseTransport] = None,
113117
cache_strategy: Optional[BaseCacheStrategy] = None,
114118
http_cache: bool = True,
115119
throttler: Optional[BaseThrottler] = None,
@@ -132,6 +136,8 @@ def __init__(
132136
ssl_verify: Union[bool, "ssl.SSLContext"] = ...,
133137
trust_env: bool = True,
134138
proxy: Optional[ProxyTypes] = None,
139+
transport: Optional[httpx.BaseTransport] = None,
140+
async_transport: Optional[httpx.AsyncBaseTransport] = None,
135141
cache_strategy: Optional[BaseCacheStrategy] = None,
136142
http_cache: bool = True,
137143
throttler: Optional[BaseThrottler] = None,
@@ -153,6 +159,8 @@ def __init__(
153159
ssl_verify: Union[bool, "ssl.SSLContext"] = True,
154160
trust_env: bool = True,
155161
proxy: Optional[ProxyTypes] = None,
162+
transport: Optional[httpx.BaseTransport] = None,
163+
async_transport: Optional[httpx.AsyncBaseTransport] = None,
156164
cache_strategy: Optional[BaseCacheStrategy] = None,
157165
http_cache: bool = True,
158166
throttler: Optional[BaseThrottler] = None,
@@ -174,6 +182,8 @@ def __init__(
174182
ssl_verify=ssl_verify,
175183
trust_env=trust_env,
176184
proxy=proxy,
185+
transport=transport,
186+
async_transport=async_transport,
177187
cache_strategy=cache_strategy,
178188
http_cache=http_cache,
179189
throttler=throttler,
@@ -241,11 +251,14 @@ def _create_sync_client(self) -> httpx.Client:
241251
if self.config.http_cache:
242252
return hishel.CacheClient(
243253
**self._get_client_defaults(),
254+
transport=self.config.transport,
244255
storage=self.config.cache_strategy.get_hishel_storage(),
245256
controller=self.config.cache_strategy.get_hishel_controller(),
246257
)
247258

248-
return httpx.Client(**self._get_client_defaults())
259+
return httpx.Client(
260+
**self._get_client_defaults(), transport=self.config.transport
261+
)
249262

250263
# get or create sync client
251264
@contextmanager
@@ -263,11 +276,14 @@ def _create_async_client(self) -> httpx.AsyncClient:
263276
if self.config.http_cache:
264277
return hishel.AsyncCacheClient(
265278
**self._get_client_defaults(),
279+
transport=self.config.async_transport,
266280
storage=self.config.cache_strategy.get_async_hishel_storage(),
267281
controller=self.config.cache_strategy.get_hishel_controller(),
268282
)
269283

270-
return httpx.AsyncClient(**self._get_client_defaults())
284+
return httpx.AsyncClient(
285+
**self._get_client_defaults(), transport=self.config.async_transport
286+
)
271287

272288
# get or create async client
273289
@asynccontextmanager

githubkit/github.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ def __init__(
7979
ssl_verify: Union[bool, "ssl.SSLContext"] = ...,
8080
trust_env: bool = True,
8181
proxy: Optional[ProxyTypes] = None,
82+
transport: Optional[httpx.BaseTransport] = None,
83+
async_transport: Optional[httpx.AsyncBaseTransport] = None,
8284
cache_strategy: Optional["BaseCacheStrategy"] = None,
8385
http_cache: bool = True,
8486
throttler: Optional["BaseThrottler"] = None,
@@ -101,6 +103,8 @@ def __init__(
101103
ssl_verify: Union[bool, "ssl.SSLContext"] = ...,
102104
trust_env: bool = True,
103105
proxy: Optional[ProxyTypes] = None,
106+
transport: Optional[httpx.BaseTransport] = None,
107+
async_transport: Optional[httpx.AsyncBaseTransport] = None,
104108
cache_strategy: Optional["BaseCacheStrategy"] = None,
105109
http_cache: bool = True,
106110
throttler: Optional["BaseThrottler"] = None,
@@ -123,6 +127,8 @@ def __init__(
123127
ssl_verify: Union[bool, "ssl.SSLContext"] = ...,
124128
trust_env: bool = True,
125129
proxy: Optional[ProxyTypes] = None,
130+
transport: Optional[httpx.BaseTransport] = None,
131+
async_transport: Optional[httpx.AsyncBaseTransport] = None,
126132
cache_strategy: Optional["BaseCacheStrategy"] = None,
127133
http_cache: bool = True,
128134
throttler: Optional["BaseThrottler"] = None,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import json
2+
from pathlib import Path
3+
4+
import httpx
5+
import pytest
6+
7+
from githubkit import GitHub
8+
from githubkit.versions.latest.models import FullRepository
9+
10+
FAKE_RESPONSE = json.loads((Path(__file__).parent / "fake_response.json").read_text())
11+
12+
13+
def target_sync_func(github: GitHub):
14+
resp = github.rest.repos.get("owner", "repo")
15+
return resp.parsed_data
16+
17+
18+
def mock_transport_handler(request: httpx.Request) -> httpx.Response:
19+
if request.method == "GET" and request.url.path == "/repos/owner/repo":
20+
return httpx.Response(status_code=200, json=FAKE_RESPONSE)
21+
raise RuntimeError(f"Unexpected request: {request.method} {request.url.path}")
22+
23+
24+
def test_sync_mock():
25+
g = GitHub("xxxxx", transport=httpx.MockTransport(mock_transport_handler))
26+
repo = target_sync_func(g)
27+
assert isinstance(repo, FullRepository)
28+
29+
30+
async def target_async_func(github: GitHub):
31+
resp = await github.rest.repos.async_get("owner", "repo")
32+
return resp.parsed_data
33+
34+
35+
@pytest.mark.anyio
36+
async def test_async_mock():
37+
g = GitHub("xxxxx", async_transport=httpx.MockTransport(mock_transport_handler))
38+
repo = await target_async_func(g)
39+
assert isinstance(repo, FullRepository)

0 commit comments

Comments
 (0)