From 8866482af8e69481855c27800d0716889762ee66 Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Sun, 5 Apr 2026 15:16:59 +0530 Subject: [PATCH 1/9] feat: allow injecting custom HTTP clients for advanced transport and retry configurations --- openapi_python_sdk/async_client.py | 4 ++-- openapi_python_sdk/async_oauth_client.py | 4 ++-- openapi_python_sdk/client.py | 4 ++-- openapi_python_sdk/oauth_client.py | 4 ++-- tests/test_async_client.py | 10 ++++++++++ tests/test_client.py | 10 ++++++++++ 6 files changed, 28 insertions(+), 8 deletions(-) diff --git a/openapi_python_sdk/async_client.py b/openapi_python_sdk/async_client.py index 7ff6e6a..608cbbc 100644 --- a/openapi_python_sdk/async_client.py +++ b/openapi_python_sdk/async_client.py @@ -10,8 +10,8 @@ class AsyncClient: Suitable for use with FastAPI, aiohttp, etc. """ - def __init__(self, token: str): - self.client = httpx.AsyncClient() + def __init__(self, token: str, client: Any = None): + self.client = client if client is not None else httpx.AsyncClient() self.auth_header: str = f"Bearer {token}" self.headers: Dict[str, str] = { "Authorization": self.auth_header, diff --git a/openapi_python_sdk/async_oauth_client.py b/openapi_python_sdk/async_oauth_client.py index 2633c93..c3234cf 100644 --- a/openapi_python_sdk/async_oauth_client.py +++ b/openapi_python_sdk/async_oauth_client.py @@ -12,8 +12,8 @@ class AsyncOauthClient: Suitable for use with FastAPI, aiohttp, etc. """ - def __init__(self, username: str, apikey: str, test: bool = False): - self.client = httpx.AsyncClient() + def __init__(self, username: str, apikey: str, test: bool = False, client: Any = None): + self.client = client if client is not None else httpx.AsyncClient() self.url: str = TEST_OAUTH_BASE_URL if test else OAUTH_BASE_URL self.auth_header: str = ( "Basic " + base64.b64encode(f"{username}:{apikey}".encode("utf-8")).decode() diff --git a/openapi_python_sdk/client.py b/openapi_python_sdk/client.py index 16956cf..aa58ee1 100644 --- a/openapi_python_sdk/client.py +++ b/openapi_python_sdk/client.py @@ -14,8 +14,8 @@ class Client: Synchronous client for making authenticated requests to Openapi endpoints. """ - def __init__(self, token: str): - self.client = httpx.Client() + def __init__(self, token: str, client: Any = None): + self.client = client if client is not None else httpx.Client() self.auth_header: str = f"Bearer {token}" self.headers: Dict[str, str] = { "Authorization": self.auth_header, diff --git a/openapi_python_sdk/oauth_client.py b/openapi_python_sdk/oauth_client.py index 8c8c446..61b583b 100644 --- a/openapi_python_sdk/oauth_client.py +++ b/openapi_python_sdk/oauth_client.py @@ -12,8 +12,8 @@ class OauthClient: Synchronous client for handling Openapi authentication and token management. """ - def __init__(self, username: str, apikey: str, test: bool = False): - self.client = httpx.Client() + def __init__(self, username: str, apikey: str, test: bool = False, client: Any = None): + self.client = client if client is not None else httpx.Client() self.url: str = TEST_OAUTH_BASE_URL if test else OAUTH_BASE_URL self.auth_header: str = ( "Basic " + base64.b64encode(f"{username}:{apikey}".encode("utf-8")).decode() diff --git a/tests/test_async_client.py b/tests/test_async_client.py index d25454b..461305b 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -42,6 +42,11 @@ async def test_get_scopes(self, mock_httpx): await oauth.aclose() mock_httpx.return_value.aclose.assert_called_once() + def test_custom_client_transport(self): + custom_client = MagicMock() + oauth = AsyncOauthClient(username="user", apikey="key", client=custom_client) + self.assertEqual(oauth.client, custom_client) + class TestAsyncClient(unittest.IsolatedAsyncioTestCase): """ @@ -85,6 +90,11 @@ async def test_request_post(self, mock_httpx): await client.aclose() mock_httpx.return_value.aclose.assert_called_once() + def test_custom_client_transport(self): + custom_client = MagicMock() + client = AsyncClient(token="abc123", client=custom_client) + self.assertEqual(client.client, custom_client) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_client.py b/tests/test_client.py index 75fe1e4..3b8f874 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -56,6 +56,11 @@ def test_auth_header_is_basic(self, mock_httpx): oauth = OauthClient(username="user", apikey="key") self.assertTrue(oauth.auth_header.startswith("Basic ")) + def test_custom_client_transport(self): + custom_client = MagicMock() + oauth = OauthClient(username="user", apikey="key", client=custom_client) + self.assertEqual(oauth.client, custom_client) + class TestClient(unittest.TestCase): @@ -109,6 +114,11 @@ def test_defaults_on_empty_request(self, mock_httpx): method="GET", url="", headers=client.headers, json={}, params={} ) + def test_custom_client_transport(self): + custom_client = MagicMock() + client = Client(token="tok", client=custom_client) + self.assertEqual(client.client, custom_client) + if __name__ == "__main__": unittest.main() From 7499139a216555c0c7ffa8a77c83d851295b2b08 Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Sun, 5 Apr 2026 15:17:56 +0530 Subject: [PATCH 2/9] docs: add instructions for customizing transport layer and retry logic --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 39db3d5..b48d866 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,26 @@ resp = client.request( ) ``` +### Customizing the Transport Layer + +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: + +```python +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry +from openapi_python_sdk import Client +import requests + +retry = Retry(total=3) +adapter = HTTPAdapter(max_retries=retry) + +session = requests.Session() +session.mount("https://", adapter) + +# Pass the custom session to the Client explicitly +client = Client("token", client=session) +``` + ## Async Usage The SDK provides `AsyncClient` and `AsyncOauthClient` for use with asynchronous frameworks like FastAPI or `aiohttp`. From 2318096ed16f787861bfe919fa6408b1ab5ac3a8 Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Sun, 5 Apr 2026 15:34:50 +0530 Subject: [PATCH 3/9] fix(client): use urllib to encode query params and resolve 400 Bad Request with special characters --- openapi_python_sdk/async_client.py | 7 +++++++ openapi_python_sdk/client.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/openapi_python_sdk/async_client.py b/openapi_python_sdk/async_client.py index 608cbbc..e0d0a05 100644 --- a/openapi_python_sdk/async_client.py +++ b/openapi_python_sdk/async_client.py @@ -43,6 +43,13 @@ async def request( payload = payload or {} params = params or {} url = url or "" + + if params: + import urllib.parse + query_string = urllib.parse.urlencode(params, doseq=True) + url = f"{url}&{query_string}" if "?" in url else f"{url}?{query_string}" + params = None + resp = await self.client.request( method=method, url=url, diff --git a/openapi_python_sdk/client.py b/openapi_python_sdk/client.py index aa58ee1..4b3cbd6 100644 --- a/openapi_python_sdk/client.py +++ b/openapi_python_sdk/client.py @@ -47,6 +47,13 @@ def request( payload = payload or {} params = params or {} url = url or "" + + if params: + import urllib.parse + query_string = urllib.parse.urlencode(params, doseq=True) + url = f"{url}&{query_string}" if "?" in url else f"{url}?{query_string}" + params = None + data = self.client.request( method=method, url=url, From 0bc56fe871b0236d9079f7aff8e2c5f8fd49d7bd Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Thu, 9 Apr 2026 22:56:50 +0530 Subject: [PATCH 4/9] make OauthClient thread-safe using threading.local --- openapi_python_sdk/oauth_client.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/openapi_python_sdk/oauth_client.py b/openapi_python_sdk/oauth_client.py index 61b583b..14c507a 100644 --- a/openapi_python_sdk/oauth_client.py +++ b/openapi_python_sdk/oauth_client.py @@ -1,4 +1,5 @@ import base64 +import threading from typing import Any, Dict, List import httpx @@ -13,7 +14,8 @@ class OauthClient: """ def __init__(self, username: str, apikey: str, test: bool = False, client: Any = None): - self.client = client if client is not None else httpx.Client() + self._client = client + self._thread_local = threading.local() self.url: str = TEST_OAUTH_BASE_URL if test else OAUTH_BASE_URL self.auth_header: str = ( "Basic " + base64.b64encode(f"{username}:{apikey}".encode("utf-8")).decode() @@ -23,6 +25,24 @@ def __init__(self, username: str, apikey: str, test: bool = False, client: Any = "Content-Type": "application/json", } + @property + def client(self) -> Any: + """ + Thread-safe access to the underlying HTTP client. + If a custom client was provided at initialization, it is returned. + Otherwise, a thread-local httpx.Client is created and returned. + """ + if self._client is not None: + return self._client + + if not hasattr(self._thread_local, "client"): + self._thread_local.client = httpx.Client() + return self._thread_local.client + + @client.setter + def client(self, value: Any): + self._client = value + def __enter__(self): """Enable use as a synchronous context manager.""" return self From 7ff7eb8a96bdc4fd62a2b88fc8bb15557af0f80d Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Thu, 9 Apr 2026 22:57:18 +0530 Subject: [PATCH 5/9] implement thread-local connection handling for shared instances --- openapi_python_sdk/client.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/openapi_python_sdk/client.py b/openapi_python_sdk/client.py index 4b3cbd6..e012f85 100644 --- a/openapi_python_sdk/client.py +++ b/openapi_python_sdk/client.py @@ -1,4 +1,5 @@ import json +import threading from typing import Any, Dict import httpx @@ -15,13 +16,32 @@ class Client: """ def __init__(self, token: str, client: Any = None): - self.client = client if client is not None else httpx.Client() + self._client = client + self._thread_local = threading.local() self.auth_header: str = f"Bearer {token}" self.headers: Dict[str, str] = { "Authorization": self.auth_header, "Content-Type": "application/json", } + @property + def client(self) -> Any: + """ + Thread-safe access to the underlying HTTP client. + If a custom client was provided at initialization, it is returned. + Otherwise, a thread-local httpx.Client is created and returned. + """ + if self._client is not None: + return self._client + + if not hasattr(self._thread_local, "client"): + self._thread_local.client = httpx.Client() + return self._thread_local.client + + @client.setter + def client(self, value: Any): + self._client = value + def __enter__(self): """Enable use as a synchronous context manager.""" return self From 91f06f3899c22aed821a4f66cbb865bfd4b1e9fa Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Thu, 9 Apr 2026 22:57:47 +0530 Subject: [PATCH 6/9] test: add thread-safety verification suite for sync clients --- tests/test_thread_safety.py | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/test_thread_safety.py diff --git a/tests/test_thread_safety.py b/tests/test_thread_safety.py new file mode 100644 index 0000000..12f22d1 --- /dev/null +++ b/tests/test_thread_safety.py @@ -0,0 +1,56 @@ +import threading +import unittest +from openapi_python_sdk import Client, OauthClient +import httpx + +class TestThreadSafety(unittest.TestCase): + def test_oauth_client_thread_safety(self): + oauth = OauthClient(username="user", apikey="key") + + clients = [] + def get_client(): + clients.append(oauth.client) + + threads = [threading.Thread(target=get_client) for _ in range(5)] + for t in threads: t.start() + for t in threads: t.join() + + # Each thread should have gotten a unique client instance + self.assertEqual(len(clients), 5) + self.assertEqual(len(set(id(c) for c in clients)), 5) + + def test_client_thread_safety(self): + client = Client(token="tok") + + clients = [] + def get_client(): + clients.append(client.client) + + threads = [threading.Thread(target=get_client) for _ in range(5)] + for t in threads: t.start() + for t in threads: t.join() + + # Each thread should have gotten a unique client instance + self.assertEqual(len(clients), 5) + self.assertEqual(len(set(id(c) for c in clients)), 5) + + def test_shared_client_injection_still_works(self): + # If we explicitly pass a client, it SHOULD be shared (backward compatibility) + shared_engine = httpx.Client() + oauth = OauthClient(username="user", apikey="key", client=shared_engine) + + clients = [] + def get_client(): + clients.append(oauth.client) + + threads = [threading.Thread(target=get_client) for _ in range(5)] + for t in threads: t.start() + for t in threads: t.join() + + # All threads should have the SAME instance because it was injected + self.assertEqual(len(clients), 5) + self.assertEqual(len(set(id(c) for c in clients)), 1) + self.assertEqual(id(clients[0]), id(shared_engine)) + +if __name__ == "__main__": + unittest.main() From 4a82a090b6e8beff38ab895fad27670d93738cac Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Thu, 9 Apr 2026 23:12:21 +0530 Subject: [PATCH 7/9] fixed split loop statement issue --- tests/test_thread_safety.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/tests/test_thread_safety.py b/tests/test_thread_safety.py index 12f22d1..661bc1f 100644 --- a/tests/test_thread_safety.py +++ b/tests/test_thread_safety.py @@ -6,30 +6,34 @@ class TestThreadSafety(unittest.TestCase): def test_oauth_client_thread_safety(self): oauth = OauthClient(username="user", apikey="key") - + clients = [] def get_client(): clients.append(oauth.client) - + threads = [threading.Thread(target=get_client) for _ in range(5)] - for t in threads: t.start() - for t in threads: t.join() - + for t in threads: + t.start() + for t in threads: + t.join() + # Each thread should have gotten a unique client instance self.assertEqual(len(clients), 5) self.assertEqual(len(set(id(c) for c in clients)), 5) def test_client_thread_safety(self): client = Client(token="tok") - + clients = [] def get_client(): clients.append(client.client) - + threads = [threading.Thread(target=get_client) for _ in range(5)] - for t in threads: t.start() - for t in threads: t.join() - + for t in threads: + t.start() + for t in threads: + t.join() + # Each thread should have gotten a unique client instance self.assertEqual(len(clients), 5) self.assertEqual(len(set(id(c) for c in clients)), 5) @@ -38,15 +42,17 @@ def test_shared_client_injection_still_works(self): # If we explicitly pass a client, it SHOULD be shared (backward compatibility) shared_engine = httpx.Client() oauth = OauthClient(username="user", apikey="key", client=shared_engine) - + clients = [] def get_client(): clients.append(oauth.client) - + threads = [threading.Thread(target=get_client) for _ in range(5)] - for t in threads: t.start() - for t in threads: t.join() - + for t in threads: + t.start() + for t in threads: + t.join() + # All threads should have the SAME instance because it was injected self.assertEqual(len(clients), 5) self.assertEqual(len(set(id(c) for c in clients)), 1) From 1d47338937381a06abb8434faf80f255a5e3fd8e Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Thu, 9 Apr 2026 23:19:00 +0530 Subject: [PATCH 8/9] fix ruff linting issues and sort imports in thread-safety tests --- openapi_python_sdk/client.py | 2 +- openapi_python_sdk/oauth_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi_python_sdk/client.py b/openapi_python_sdk/client.py index e012f85..5393fda 100644 --- a/openapi_python_sdk/client.py +++ b/openapi_python_sdk/client.py @@ -33,7 +33,7 @@ def client(self) -> Any: """ if self._client is not None: return self._client - + if not hasattr(self._thread_local, "client"): self._thread_local.client = httpx.Client() return self._thread_local.client diff --git a/openapi_python_sdk/oauth_client.py b/openapi_python_sdk/oauth_client.py index 14c507a..aa43964 100644 --- a/openapi_python_sdk/oauth_client.py +++ b/openapi_python_sdk/oauth_client.py @@ -34,7 +34,7 @@ def client(self) -> Any: """ if self._client is not None: return self._client - + if not hasattr(self._thread_local, "client"): self._thread_local.client = httpx.Client() return self._thread_local.client From 7c7dc3793d9923a03ffceaa18de3ccd1ea8d214c Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Thu, 9 Apr 2026 23:23:29 +0530 Subject: [PATCH 9/9] fix ruff linting issues and sort imports in thread-safety tests --- openapi_python_sdk/oauth_client.py | 1 - tests/test_thread_safety.py | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openapi_python_sdk/oauth_client.py b/openapi_python_sdk/oauth_client.py index aa43964..bcb44da 100644 --- a/openapi_python_sdk/oauth_client.py +++ b/openapi_python_sdk/oauth_client.py @@ -34,7 +34,6 @@ def client(self) -> Any: """ if self._client is not None: return self._client - if not hasattr(self._thread_local, "client"): self._thread_local.client = httpx.Client() return self._thread_local.client diff --git a/tests/test_thread_safety.py b/tests/test_thread_safety.py index 661bc1f..5763084 100644 --- a/tests/test_thread_safety.py +++ b/tests/test_thread_safety.py @@ -1,8 +1,11 @@ import threading import unittest -from openapi_python_sdk import Client, OauthClient + import httpx +from openapi_python_sdk import Client, OauthClient + + class TestThreadSafety(unittest.TestCase): def test_oauth_client_thread_safety(self): oauth = OauthClient(username="user", apikey="key")