Skip to content

Commit 512e43d

Browse files
Merge pull request #16 from Deadpool2000/fix-issue-9
Fix issue #9: Fix 400 Bad Request on API calls containing special characters in query parameters
2 parents 5637eba + 2318096 commit 512e43d

7 files changed

Lines changed: 62 additions & 8 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,26 @@ resp = client.request(
112112
)
113113
```
114114

115+
### Customizing the Transport Layer
116+
117+
If you need to configure custom retry logic, proxies, or use a different HTTP client (such as passing a `requests.Session` with a custom urllib3 `Retry`), you can inject it directly using the `client` parameter on any SDK class:
118+
119+
```python
120+
from requests.adapters import HTTPAdapter
121+
from urllib3.util.retry import Retry
122+
from openapi_python_sdk import Client
123+
import requests
124+
125+
retry = Retry(total=3)
126+
adapter = HTTPAdapter(max_retries=retry)
127+
128+
session = requests.Session()
129+
session.mount("https://", adapter)
130+
131+
# Pass the custom session to the Client explicitly
132+
client = Client("token", client=session)
133+
```
134+
115135
## Async Usage
116136

117137
The SDK provides `AsyncClient` and `AsyncOauthClient` for use with asynchronous frameworks like FastAPI or `aiohttp`.

openapi_python_sdk/async_client.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class AsyncClient:
1010
Suitable for use with FastAPI, aiohttp, etc.
1111
"""
1212

13-
def __init__(self, token: str):
14-
self.client = httpx.AsyncClient()
13+
def __init__(self, token: str, client: Any = None):
14+
self.client = client if client is not None else httpx.AsyncClient()
1515
self.auth_header: str = f"Bearer {token}"
1616
self.headers: Dict[str, str] = {
1717
"Authorization": self.auth_header,
@@ -43,6 +43,13 @@ async def request(
4343
payload = payload or {}
4444
params = params or {}
4545
url = url or ""
46+
47+
if params:
48+
import urllib.parse
49+
query_string = urllib.parse.urlencode(params, doseq=True)
50+
url = f"{url}&{query_string}" if "?" in url else f"{url}?{query_string}"
51+
params = None
52+
4653
resp = await self.client.request(
4754
method=method,
4855
url=url,

openapi_python_sdk/async_oauth_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class AsyncOauthClient:
1212
Suitable for use with FastAPI, aiohttp, etc.
1313
"""
1414

15-
def __init__(self, username: str, apikey: str, test: bool = False):
16-
self.client = httpx.AsyncClient()
15+
def __init__(self, username: str, apikey: str, test: bool = False, client: Any = None):
16+
self.client = client if client is not None else httpx.AsyncClient()
1717
self.url: str = TEST_OAUTH_BASE_URL if test else OAUTH_BASE_URL
1818
self.auth_header: str = (
1919
"Basic " + base64.b64encode(f"{username}:{apikey}".encode("utf-8")).decode()

openapi_python_sdk/client.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ class Client:
1414
Synchronous client for making authenticated requests to Openapi endpoints.
1515
"""
1616

17-
def __init__(self, token: str):
18-
self.client = httpx.Client()
17+
def __init__(self, token: str, client: Any = None):
18+
self.client = client if client is not None else httpx.Client()
1919
self.auth_header: str = f"Bearer {token}"
2020
self.headers: Dict[str, str] = {
2121
"Authorization": self.auth_header,
@@ -47,6 +47,13 @@ def request(
4747
payload = payload or {}
4848
params = params or {}
4949
url = url or ""
50+
51+
if params:
52+
import urllib.parse
53+
query_string = urllib.parse.urlencode(params, doseq=True)
54+
url = f"{url}&{query_string}" if "?" in url else f"{url}?{query_string}"
55+
params = None
56+
5057
data = self.client.request(
5158
method=method,
5259
url=url,

openapi_python_sdk/oauth_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class OauthClient:
1212
Synchronous client for handling Openapi authentication and token management.
1313
"""
1414

15-
def __init__(self, username: str, apikey: str, test: bool = False):
16-
self.client = httpx.Client()
15+
def __init__(self, username: str, apikey: str, test: bool = False, client: Any = None):
16+
self.client = client if client is not None else httpx.Client()
1717
self.url: str = TEST_OAUTH_BASE_URL if test else OAUTH_BASE_URL
1818
self.auth_header: str = (
1919
"Basic " + base64.b64encode(f"{username}:{apikey}".encode("utf-8")).decode()

tests/test_async_client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ async def test_get_scopes(self, mock_httpx):
4242
await oauth.aclose()
4343
mock_httpx.return_value.aclose.assert_called_once()
4444

45+
def test_custom_client_transport(self):
46+
custom_client = MagicMock()
47+
oauth = AsyncOauthClient(username="user", apikey="key", client=custom_client)
48+
self.assertEqual(oauth.client, custom_client)
49+
4550

4651
class TestAsyncClient(unittest.IsolatedAsyncioTestCase):
4752
"""
@@ -85,6 +90,11 @@ async def test_request_post(self, mock_httpx):
8590
await client.aclose()
8691
mock_httpx.return_value.aclose.assert_called_once()
8792

93+
def test_custom_client_transport(self):
94+
custom_client = MagicMock()
95+
client = AsyncClient(token="abc123", client=custom_client)
96+
self.assertEqual(client.client, custom_client)
97+
8898

8999
if __name__ == "__main__":
90100
unittest.main()

tests/test_client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ def test_auth_header_is_basic(self, mock_httpx):
5656
oauth = OauthClient(username="user", apikey="key")
5757
self.assertTrue(oauth.auth_header.startswith("Basic "))
5858

59+
def test_custom_client_transport(self):
60+
custom_client = MagicMock()
61+
oauth = OauthClient(username="user", apikey="key", client=custom_client)
62+
self.assertEqual(oauth.client, custom_client)
63+
5964

6065
class TestClient(unittest.TestCase):
6166

@@ -109,6 +114,11 @@ def test_defaults_on_empty_request(self, mock_httpx):
109114
method="GET", url="", headers=client.headers, json={}, params={}
110115
)
111116

117+
def test_custom_client_transport(self):
118+
custom_client = MagicMock()
119+
client = Client(token="tok", client=custom_client)
120+
self.assertEqual(client.client, custom_client)
121+
112122

113123
if __name__ == "__main__":
114124
unittest.main()

0 commit comments

Comments
 (0)