From c0aebaa5e01119ca4834d69614185969b85892ef Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Thu, 9 Apr 2026 22:56:50 +0530 Subject: [PATCH 1/6] make OauthClient thread-safe using threading.local --- openapi_python_sdk/oauth_client.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/openapi_python_sdk/oauth_client.py b/openapi_python_sdk/oauth_client.py index f6a418d..6b4b2ab 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,9 @@ class OauthClient: """ def __init__(self, username: str, apikey: str, test: bool = False, client: Any = None, timeout: float = 30.0): - self.client = client if client is not None else httpx.Client(timeout=timeout) + self._client = client + self._thread_local = threading.local() + self.timeout = timeout 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 +26,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(timeout=self.timeout) + 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 e4f5d4f44f30ba9f1cbebcb036bc52c719cef3b4 Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Thu, 9 Apr 2026 22:57:18 +0530 Subject: [PATCH 2/6] implement thread-local connection handling for shared instances --- openapi_python_sdk/client.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/openapi_python_sdk/client.py b/openapi_python_sdk/client.py index 18b6341..cbaeabe 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,33 @@ class Client: """ def __init__(self, token: str, client: Any = None, timeout: float = 30.0): - self.client = client if client is not None else httpx.Client(timeout=timeout) + self._client = client + self._thread_local = threading.local() + self.timeout = timeout 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(timeout=self.timeout) + 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 d6b0211b7c799fe4b35cefc07c9a44c9a58a4cd9 Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Thu, 9 Apr 2026 22:57:47 +0530 Subject: [PATCH 3/6] 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 38278067424bbb82545bedcf41eebd33f9ffbc83 Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Thu, 9 Apr 2026 23:12:21 +0530 Subject: [PATCH 4/6] 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 90bf9fc1046eaf144689e2f08c72727facc41991 Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Thu, 9 Apr 2026 23:19:00 +0530 Subject: [PATCH 5/6] 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 cbaeabe..4ba5211 100644 --- a/openapi_python_sdk/client.py +++ b/openapi_python_sdk/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(timeout=self.timeout) return self._thread_local.client diff --git a/openapi_python_sdk/oauth_client.py b/openapi_python_sdk/oauth_client.py index 6b4b2ab..f28fc0a 100644 --- a/openapi_python_sdk/oauth_client.py +++ b/openapi_python_sdk/oauth_client.py @@ -35,7 +35,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(timeout=self.timeout) return self._thread_local.client From 81cd2a8fc0e053d836a01e059a7fe9446231c75a Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Thu, 9 Apr 2026 23:23:29 +0530 Subject: [PATCH 6/6] 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 f28fc0a..a3be9db 100644 --- a/openapi_python_sdk/oauth_client.py +++ b/openapi_python_sdk/oauth_client.py @@ -35,7 +35,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(timeout=self.timeout) 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")