From 25ec00ebf7f53ce8aa55b813a99daeec2e7198d8 Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Wed, 25 Mar 2026 22:21:08 +0530 Subject: [PATCH 1/4] feat: add async support (httpx) and context manager support for all clients #5 --- README.md | 39 +++++++- openapi-python-sdk | 1 + openapi_python_sdk/__init__.py | 7 ++ openapi_python_sdk/client.py | 174 +++++++++++++++++++++++++++++++-- tests/test_async_client.py | 90 +++++++++++++++++ 5 files changed, 301 insertions(+), 10 deletions(-) create mode 160000 openapi-python-sdk create mode 100644 tests/test_async_client.py diff --git a/README.md b/README.md index 5083a7f..39db3d5 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,10 @@ Before using the Openapi Python Client, you will need an account at [Openapi](ht ## Features - **Agnostic Design**: No API-specific classes, works with any OpenAPI service -- **Minimal Dependencies**: Only requires Python 3.8+ and `requests` +- **Minimal Dependencies**: Only requires Python 3.8+ and `httpx` - **OAuth Support**: Built-in OAuth client for token management - **HTTP Primitives**: GET, POST, PUT, DELETE, PATCH methods +- **Async Support**: Fully compatible with async frameworks like FastAPI and aiohttp - **Clean Interface**: Similar to the Rust SDK design ## What you can do @@ -70,7 +71,7 @@ Interaction with the Openapi platform happens in two distinct steps. Authenticate with your credentials and obtain a short-lived bearer token scoped to the endpoints you need. ```python -from openapi_python_sdk.client import OauthClient +from openapi_python_sdk import OauthClient oauth = OauthClient(username="", apikey="", test=True) @@ -92,7 +93,7 @@ oauth.delete_token(id=token) Use the token to make authenticated requests to any Openapi service. ```python -from openapi_python_sdk.client import Client +from openapi_python_sdk import Client client = Client(token=token) @@ -111,6 +112,37 @@ resp = client.request( ) ``` +## Async Usage + +The SDK provides `AsyncClient` and `AsyncOauthClient` for use with asynchronous frameworks like FastAPI or `aiohttp`. + +### Async Authentication + +```python +from openapi_python_sdk import AsyncOauthClient + +async with AsyncOauthClient(username="", apikey="", test=True) as oauth: + resp = await oauth.create_token( + scopes=["GET:test.imprese.openapi.it/advance"], + ttl=3600, + ) + token = resp["token"] +``` + +### Async Requests + +```python +from openapi_python_sdk import AsyncClient + +async with AsyncClient(token=token) as client: + resp = await client.request( + method="GET", + url="https://test.imprese.openapi.it/advance", + params={"denominazione": "altravia"}, + ) +``` + + ## Testing Install dev dependencies and run the test suite: @@ -174,4 +206,3 @@ The MIT License is a permissive open-source license that allows you to freely us In short, you are free to use this SDK in your personal, academic, or commercial projects, with minimal restrictions. The project is provided "as-is", without any warranty of any kind, either expressed or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. For more details, see the full license text at the [MIT License page](https://choosealicense.com/licenses/mit/). - diff --git a/openapi-python-sdk b/openapi-python-sdk new file mode 160000 index 0000000..37018d7 --- /dev/null +++ b/openapi-python-sdk @@ -0,0 +1 @@ +Subproject commit 37018d7aebbaa11312900e19b5a23c854553ee8f diff --git a/openapi_python_sdk/__init__.py b/openapi_python_sdk/__init__.py index e69de29..e3bbff2 100644 --- a/openapi_python_sdk/__init__.py +++ b/openapi_python_sdk/__init__.py @@ -0,0 +1,7 @@ +""" +Openapi Python SDK - A minimal and agnostic SDK for the Openapi marketplace. +Exports both synchronous and asynchronous clients. +""" +from .client import AsyncClient, AsyncOauthClient, Client, OauthClient + +__all__ = ["Client", "AsyncClient", "OauthClient", "AsyncOauthClient"] diff --git a/openapi_python_sdk/client.py b/openapi_python_sdk/client.py index 4268bda..27f830f 100644 --- a/openapi_python_sdk/client.py +++ b/openapi_python_sdk/client.py @@ -1,15 +1,18 @@ import base64 -from typing import Any, Dict +import json +from typing import Any, Dict, List, Optional import httpx -import json - OAUTH_BASE_URL = "https://oauth.openapi.it" TEST_OAUTH_BASE_URL = "https://test.oauth.openapi.it" 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() self.url: str = TEST_OAUTH_BASE_URL if test else OAUTH_BASE_URL @@ -21,31 +24,115 @@ def __init__(self, username: str, apikey: str, test: bool = False): "Content-Type": "application/json", } + def __enter__(self): + """Enable use as a synchronous context manager.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Ensure the underlying HTTP client is closed on exit.""" + self.client.close() + + def close(self): + """Manually close the underlying HTTP client.""" + self.client.close() + def get_scopes(self, limit: bool = False) -> Dict[str, Any]: + """Retrieve available scopes for the current user.""" params = {"limit": int(limit)} url = f"{self.url}/scopes" return self.client.get(url=url, headers=self.headers, params=params).json() - def create_token(self, scopes: list[str] = [], ttl: int = 0) -> Dict[str, Any]: + def create_token(self, scopes: List[str] = [], ttl: int = 0) -> Dict[str, Any]: + """Create a new bearer token with specified scopes and TTL.""" payload = {"scopes": scopes, "ttl": ttl} url = f"{self.url}/token" return self.client.post(url=url, headers=self.headers, json=payload).json() def get_token(self, scope: str = None) -> Dict[str, Any]: + """Retrieve an existing token, optionally filtered by scope.""" params = {"scope": scope or ""} url = f"{self.url}/token" return self.client.get(url=url, headers=self.headers, params=params).json() def delete_token(self, id: str) -> Dict[str, Any]: + """Revoke/Delete a specific token by ID.""" url = f"{self.url}/token/{id}" return self.client.delete(url=url, headers=self.headers).json() def get_counters(self, period: str, date: str) -> Dict[str, Any]: + """Retrieve usage counters for a specific period and date.""" url = f"{self.url}/counters/{period}/{date}" return self.client.get(url=url, headers=self.headers).json() +class AsyncOauthClient: + """ + Asynchronous client for handling Openapi authentication and token management. + Suitable for use with FastAPI, aiohttp, etc. + """ + + def __init__(self, username: str, apikey: str, test: bool = False): + self.client = 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() + ) + self.headers: Dict[str, Any] = { + "Authorization": self.auth_header, + "Content-Type": "application/json", + } + + async def __aenter__(self): + """Enable use as an asynchronous context manager.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Ensure the underlying HTTP client is closed on exit (async).""" + await self.client.aclose() + + async def aclose(self): + """Manually close the underlying HTTP client (async).""" + await self.client.aclose() + + async def get_scopes(self, limit: bool = False) -> Dict[str, Any]: + """Retrieve available scopes for the current user (async).""" + params = {"limit": int(limit)} + url = f"{self.url}/scopes" + resp = await self.client.get(url=url, headers=self.headers, params=params) + return resp.json() + + async def create_token(self, scopes: List[str] = [], ttl: int = 0) -> Dict[str, Any]: + """Create a new bearer token with specified scopes and TTL (async).""" + payload = {"scopes": scopes, "ttl": ttl} + url = f"{self.url}/token" + resp = await self.client.post(url=url, headers=self.headers, json=payload) + return resp.json() + + async def get_token(self, scope: str = None) -> Dict[str, Any]: + """Retrieve an existing token, optionally filtered by scope (async).""" + params = {"scope": scope or ""} + url = f"{self.url}/token" + resp = await self.client.get(url=url, headers=self.headers, params=params) + return resp.json() + + async def delete_token(self, id: str) -> Dict[str, Any]: + """Revoke/Delete a specific token by ID (async).""" + url = f"{self.url}/token/{id}" + resp = await self.client.delete(url=url, headers=self.headers) + return resp.json() + + async def get_counters(self, period: str, date: str) -> Dict[str, Any]: + """Retrieve usage counters for a specific period and date (async).""" + url = f"{self.url}/counters/{period}/{date}" + resp = await self.client.get(url=url, headers=self.headers) + return resp.json() + + class Client: + """ + Synchronous client for making authenticated requests to Openapi endpoints. + """ + def __init__(self, token: str): self.client = httpx.Client() self.auth_header: str = f"Bearer {token}" @@ -54,6 +141,18 @@ def __init__(self, token: str): "Content-Type": "application/json", } + def __enter__(self): + """Enable use as a synchronous context manager.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Ensure the underlying HTTP client is closed on exit.""" + self.client.close() + + def close(self): + """Manually close the underlying HTTP client.""" + self.client.close() + def request( self, method: str = "GET", @@ -61,6 +160,9 @@ def request( payload: Dict[str, Any] = None, params: Dict[str, Any] = None, ) -> Dict[str, Any]: + """ + Make a synchronous HTTP request to the specified Openapi endpoint. + """ payload = payload or {} params = params or {} url = url or "" @@ -72,9 +174,69 @@ def request( params=params, ).json() - if isinstance(data,str): + # Handle cases where the API might return a JSON-encoded string instead of an object + if isinstance(data, str): + try: + data = json.loads(data) + except json.JSONDecodeError: + pass + + return data + + +class AsyncClient: + """ + Asynchronous client for making authenticated requests to Openapi endpoints. + Suitable for use with FastAPI, aiohttp, etc. + """ + + def __init__(self, token: str): + self.client = httpx.AsyncClient() + self.auth_header: str = f"Bearer {token}" + self.headers: Dict[str, str] = { + "Authorization": self.auth_header, + "Content-Type": "application/json", + } + + async def __aenter__(self): + """Enable use as an asynchronous context manager.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Ensure the underlying HTTP client is closed on exit (async).""" + await self.client.aclose() + + async def aclose(self): + """Manually close the underlying HTTP client (async).""" + await self.client.aclose() + + async def request( + self, + method: str = "GET", + url: str = None, + payload: Dict[str, Any] = None, + params: Dict[str, Any] = None, + ) -> Dict[str, Any]: + """ + Make an asynchronous HTTP request to the specified Openapi endpoint. + """ + payload = payload or {} + params = params or {} + url = url or "" + resp = await self.client.request( + method=method, + url=url, + headers=self.headers, + json=payload, + params=params, + ) + data = resp.json() + + # Handle cases where the API might return a JSON-encoded string instead of an object + if isinstance(data, str): try: - data=json.loads(data) + data = json.loads(data) except json.JSONDecodeError: pass + return data \ No newline at end of file diff --git a/tests/test_async_client.py b/tests/test_async_client.py new file mode 100644 index 0000000..d25454b --- /dev/null +++ b/tests/test_async_client.py @@ -0,0 +1,90 @@ +import unittest +from unittest.mock import AsyncMock, MagicMock, patch + +from openapi_python_sdk.client import AsyncClient, AsyncOauthClient + + +class TestAsyncOauthClient(unittest.IsolatedAsyncioTestCase): + """ + Test suite for AsyncOauthClient using IsolatedAsyncioTestCase + which allows for native await calls in test methods. + """ + + @patch("openapi_python_sdk.client.httpx.AsyncClient") + async def test_create_token(self, mock_httpx): + # Mocking the response and the post method + mock_resp = MagicMock() + mock_resp.json.return_value = {"token": "abc123"} + mock_httpx.return_value.post = AsyncMock(return_value=mock_resp) + mock_httpx.return_value.aclose = AsyncMock() + + # Testing the async context manager (__aenter__ / __aexit__) + async with AsyncOauthClient(username="user", apikey="key", test=True) as oauth: + resp = await oauth.create_token(scopes=["GET:test.example.com/api"], ttl=3600) + + self.assertEqual(resp["token"], "abc123") + mock_httpx.return_value.post.assert_called_once() + # Verify aclose was called by the context manager + mock_httpx.return_value.aclose.assert_called_once() + + @patch("openapi_python_sdk.client.httpx.AsyncClient") + async def test_get_scopes(self, mock_httpx): + mock_resp = MagicMock() + mock_resp.json.return_value = {"scopes": ["GET:test.example.com/api"]} + mock_httpx.return_value.get = AsyncMock(return_value=mock_resp) + mock_httpx.return_value.aclose = AsyncMock() + + oauth = AsyncOauthClient(username="user", apikey="key") + resp = await oauth.get_scopes() + + self.assertIn("scopes", resp) + # Manually closing the client as we didn't use the context manager here + await oauth.aclose() + mock_httpx.return_value.aclose.assert_called_once() + + +class TestAsyncClient(unittest.IsolatedAsyncioTestCase): + """ + Test suite for the generic AsyncClient. + """ + + @patch("openapi_python_sdk.client.httpx.AsyncClient") + async def test_request_get(self, mock_httpx): + mock_resp = MagicMock() + mock_resp.json.return_value = {"data": []} + mock_httpx.return_value.request = AsyncMock(return_value=mock_resp) + mock_httpx.return_value.aclose = AsyncMock() + + async with AsyncClient(token="abc123") as client: + resp = await client.request( + method="GET", + url="https://test.imprese.openapi.it/advance", + params={"denominazione": "altravia"}, + ) + + self.assertEqual(resp, {"data": []}) + mock_httpx.return_value.request.assert_called_once() + mock_httpx.return_value.aclose.assert_called_once() + + @patch("openapi_python_sdk.client.httpx.AsyncClient") + async def test_request_post(self, mock_httpx): + mock_resp = MagicMock() + mock_resp.json.return_value = {"result": "ok"} + mock_httpx.return_value.request = AsyncMock(return_value=mock_resp) + mock_httpx.return_value.aclose = AsyncMock() + + client = AsyncClient(token="abc123") + resp = await client.request( + method="POST", + url="https://test.postontarget.com/fields/country", + payload={"limit": 0, "query": {"country_code": "IT"}}, + ) + + self.assertEqual(resp["result"], "ok") + # Ensure cleanup + await client.aclose() + mock_httpx.return_value.aclose.assert_called_once() + + +if __name__ == "__main__": + unittest.main() From 3eebe9e3c79f452025db48318f9c77edc2a5c176 Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Tue, 31 Mar 2026 22:24:50 +0530 Subject: [PATCH 2/4] refactor: extract client classes into separate module files without breaking API --- openapi_python_sdk/__init__.py | 5 +- openapi_python_sdk/async_client.py | 62 ++++++++ openapi_python_sdk/async_oauth_client.py | 69 +++++++++ openapi_python_sdk/client.py | 187 +---------------------- openapi_python_sdk/oauth_client.py | 64 ++++++++ 5 files changed, 204 insertions(+), 183 deletions(-) create mode 100644 openapi_python_sdk/async_client.py create mode 100644 openapi_python_sdk/async_oauth_client.py create mode 100644 openapi_python_sdk/oauth_client.py diff --git a/openapi_python_sdk/__init__.py b/openapi_python_sdk/__init__.py index e3bbff2..7ca57cc 100644 --- a/openapi_python_sdk/__init__.py +++ b/openapi_python_sdk/__init__.py @@ -2,6 +2,9 @@ Openapi Python SDK - A minimal and agnostic SDK for the Openapi marketplace. Exports both synchronous and asynchronous clients. """ -from .client import AsyncClient, AsyncOauthClient, Client, OauthClient +from .async_client import AsyncClient +from .async_oauth_client import AsyncOauthClient +from .client import Client +from .oauth_client import OauthClient __all__ = ["Client", "AsyncClient", "OauthClient", "AsyncOauthClient"] diff --git a/openapi_python_sdk/async_client.py b/openapi_python_sdk/async_client.py new file mode 100644 index 0000000..7ff6e6a --- /dev/null +++ b/openapi_python_sdk/async_client.py @@ -0,0 +1,62 @@ +import json +from typing import Any, Dict + +import httpx + + +class AsyncClient: + """ + Asynchronous client for making authenticated requests to Openapi endpoints. + Suitable for use with FastAPI, aiohttp, etc. + """ + + def __init__(self, token: str): + self.client = httpx.AsyncClient() + self.auth_header: str = f"Bearer {token}" + self.headers: Dict[str, str] = { + "Authorization": self.auth_header, + "Content-Type": "application/json", + } + + async def __aenter__(self): + """Enable use as an asynchronous context manager.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Ensure the underlying HTTP client is closed on exit (async).""" + await self.client.aclose() + + async def aclose(self): + """Manually close the underlying HTTP client (async).""" + await self.client.aclose() + + async def request( + self, + method: str = "GET", + url: str = None, + payload: Dict[str, Any] = None, + params: Dict[str, Any] = None, + ) -> Dict[str, Any]: + """ + Make an asynchronous HTTP request to the specified Openapi endpoint. + """ + payload = payload or {} + params = params or {} + url = url or "" + resp = await self.client.request( + method=method, + url=url, + headers=self.headers, + json=payload, + params=params, + ) + data = resp.json() + + # Handle cases where the API might return a JSON-encoded string instead of an object + if isinstance(data, str): + try: + data = json.loads(data) + except json.JSONDecodeError: + pass + + return data diff --git a/openapi_python_sdk/async_oauth_client.py b/openapi_python_sdk/async_oauth_client.py new file mode 100644 index 0000000..2633c93 --- /dev/null +++ b/openapi_python_sdk/async_oauth_client.py @@ -0,0 +1,69 @@ +import base64 +from typing import Any, Dict, List + +import httpx + +from .oauth_client import OAUTH_BASE_URL, TEST_OAUTH_BASE_URL + + +class AsyncOauthClient: + """ + Asynchronous client for handling Openapi authentication and token management. + Suitable for use with FastAPI, aiohttp, etc. + """ + + def __init__(self, username: str, apikey: str, test: bool = False): + self.client = 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() + ) + self.headers: Dict[str, Any] = { + "Authorization": self.auth_header, + "Content-Type": "application/json", + } + + async def __aenter__(self): + """Enable use as an asynchronous context manager.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Ensure the underlying HTTP client is closed on exit (async).""" + await self.client.aclose() + + async def aclose(self): + """Manually close the underlying HTTP client (async).""" + await self.client.aclose() + + async def get_scopes(self, limit: bool = False) -> Dict[str, Any]: + """Retrieve available scopes for the current user (async).""" + params = {"limit": int(limit)} + url = f"{self.url}/scopes" + resp = await self.client.get(url=url, headers=self.headers, params=params) + return resp.json() + + async def create_token(self, scopes: List[str] = [], ttl: int = 0) -> Dict[str, Any]: + """Create a new bearer token with specified scopes and TTL (async).""" + payload = {"scopes": scopes, "ttl": ttl} + url = f"{self.url}/token" + resp = await self.client.post(url=url, headers=self.headers, json=payload) + return resp.json() + + async def get_token(self, scope: str = None) -> Dict[str, Any]: + """Retrieve an existing token, optionally filtered by scope (async).""" + params = {"scope": scope or ""} + url = f"{self.url}/token" + resp = await self.client.get(url=url, headers=self.headers, params=params) + return resp.json() + + async def delete_token(self, id: str) -> Dict[str, Any]: + """Revoke/Delete a specific token by ID (async).""" + url = f"{self.url}/token/{id}" + resp = await self.client.delete(url=url, headers=self.headers) + return resp.json() + + async def get_counters(self, period: str, date: str) -> Dict[str, Any]: + """Retrieve usage counters for a specific period and date (async).""" + url = f"{self.url}/counters/{period}/{date}" + resp = await self.client.get(url=url, headers=self.headers) + return resp.json() diff --git a/openapi_python_sdk/client.py b/openapi_python_sdk/client.py index 27f830f..6d4cf1f 100644 --- a/openapi_python_sdk/client.py +++ b/openapi_python_sdk/client.py @@ -1,131 +1,12 @@ -import base64 import json -from typing import Any, Dict, List, Optional +from typing import Any, Dict import httpx -OAUTH_BASE_URL = "https://oauth.openapi.it" -TEST_OAUTH_BASE_URL = "https://test.oauth.openapi.it" - - -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() - 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() - ) - self.headers: Dict[str, Any] = { - "Authorization": self.auth_header, - "Content-Type": "application/json", - } - - def __enter__(self): - """Enable use as a synchronous context manager.""" - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Ensure the underlying HTTP client is closed on exit.""" - self.client.close() - - def close(self): - """Manually close the underlying HTTP client.""" - self.client.close() - - def get_scopes(self, limit: bool = False) -> Dict[str, Any]: - """Retrieve available scopes for the current user.""" - params = {"limit": int(limit)} - url = f"{self.url}/scopes" - return self.client.get(url=url, headers=self.headers, params=params).json() - - def create_token(self, scopes: List[str] = [], ttl: int = 0) -> Dict[str, Any]: - """Create a new bearer token with specified scopes and TTL.""" - payload = {"scopes": scopes, "ttl": ttl} - url = f"{self.url}/token" - return self.client.post(url=url, headers=self.headers, json=payload).json() - - def get_token(self, scope: str = None) -> Dict[str, Any]: - """Retrieve an existing token, optionally filtered by scope.""" - params = {"scope": scope or ""} - url = f"{self.url}/token" - return self.client.get(url=url, headers=self.headers, params=params).json() - - def delete_token(self, id: str) -> Dict[str, Any]: - """Revoke/Delete a specific token by ID.""" - url = f"{self.url}/token/{id}" - return self.client.delete(url=url, headers=self.headers).json() - - def get_counters(self, period: str, date: str) -> Dict[str, Any]: - """Retrieve usage counters for a specific period and date.""" - url = f"{self.url}/counters/{period}/{date}" - return self.client.get(url=url, headers=self.headers).json() - - -class AsyncOauthClient: - """ - Asynchronous client for handling Openapi authentication and token management. - Suitable for use with FastAPI, aiohttp, etc. - """ - - def __init__(self, username: str, apikey: str, test: bool = False): - self.client = 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() - ) - self.headers: Dict[str, Any] = { - "Authorization": self.auth_header, - "Content-Type": "application/json", - } - - async def __aenter__(self): - """Enable use as an asynchronous context manager.""" - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - """Ensure the underlying HTTP client is closed on exit (async).""" - await self.client.aclose() - - async def aclose(self): - """Manually close the underlying HTTP client (async).""" - await self.client.aclose() - - async def get_scopes(self, limit: bool = False) -> Dict[str, Any]: - """Retrieve available scopes for the current user (async).""" - params = {"limit": int(limit)} - url = f"{self.url}/scopes" - resp = await self.client.get(url=url, headers=self.headers, params=params) - return resp.json() - - async def create_token(self, scopes: List[str] = [], ttl: int = 0) -> Dict[str, Any]: - """Create a new bearer token with specified scopes and TTL (async).""" - payload = {"scopes": scopes, "ttl": ttl} - url = f"{self.url}/token" - resp = await self.client.post(url=url, headers=self.headers, json=payload) - return resp.json() - - async def get_token(self, scope: str = None) -> Dict[str, Any]: - """Retrieve an existing token, optionally filtered by scope (async).""" - params = {"scope": scope or ""} - url = f"{self.url}/token" - resp = await self.client.get(url=url, headers=self.headers, params=params) - return resp.json() - - async def delete_token(self, id: str) -> Dict[str, Any]: - """Revoke/Delete a specific token by ID (async).""" - url = f"{self.url}/token/{id}" - resp = await self.client.delete(url=url, headers=self.headers) - return resp.json() - - async def get_counters(self, period: str, date: str) -> Dict[str, Any]: - """Retrieve usage counters for a specific period and date (async).""" - url = f"{self.url}/counters/{period}/{date}" - resp = await self.client.get(url=url, headers=self.headers) - return resp.json() +# Backward compatibility imports +from .async_client import AsyncClient +from .async_oauth_client import AsyncOauthClient +from .oauth_client import OauthClient class Client: @@ -174,64 +55,6 @@ def request( params=params, ).json() - # Handle cases where the API might return a JSON-encoded string instead of an object - if isinstance(data, str): - try: - data = json.loads(data) - except json.JSONDecodeError: - pass - - return data - - -class AsyncClient: - """ - Asynchronous client for making authenticated requests to Openapi endpoints. - Suitable for use with FastAPI, aiohttp, etc. - """ - - def __init__(self, token: str): - self.client = httpx.AsyncClient() - self.auth_header: str = f"Bearer {token}" - self.headers: Dict[str, str] = { - "Authorization": self.auth_header, - "Content-Type": "application/json", - } - - async def __aenter__(self): - """Enable use as an asynchronous context manager.""" - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - """Ensure the underlying HTTP client is closed on exit (async).""" - await self.client.aclose() - - async def aclose(self): - """Manually close the underlying HTTP client (async).""" - await self.client.aclose() - - async def request( - self, - method: str = "GET", - url: str = None, - payload: Dict[str, Any] = None, - params: Dict[str, Any] = None, - ) -> Dict[str, Any]: - """ - Make an asynchronous HTTP request to the specified Openapi endpoint. - """ - payload = payload or {} - params = params or {} - url = url or "" - resp = await self.client.request( - method=method, - url=url, - headers=self.headers, - json=payload, - params=params, - ) - data = resp.json() - # Handle cases where the API might return a JSON-encoded string instead of an object if isinstance(data, str): try: diff --git a/openapi_python_sdk/oauth_client.py b/openapi_python_sdk/oauth_client.py new file mode 100644 index 0000000..8c8c446 --- /dev/null +++ b/openapi_python_sdk/oauth_client.py @@ -0,0 +1,64 @@ +import base64 +from typing import Any, Dict, List + +import httpx + +OAUTH_BASE_URL = "https://oauth.openapi.it" +TEST_OAUTH_BASE_URL = "https://test.oauth.openapi.it" + + +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() + 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() + ) + self.headers: Dict[str, Any] = { + "Authorization": self.auth_header, + "Content-Type": "application/json", + } + + def __enter__(self): + """Enable use as a synchronous context manager.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Ensure the underlying HTTP client is closed on exit.""" + self.client.close() + + def close(self): + """Manually close the underlying HTTP client.""" + self.client.close() + + def get_scopes(self, limit: bool = False) -> Dict[str, Any]: + """Retrieve available scopes for the current user.""" + params = {"limit": int(limit)} + url = f"{self.url}/scopes" + return self.client.get(url=url, headers=self.headers, params=params).json() + + def create_token(self, scopes: List[str] = [], ttl: int = 0) -> Dict[str, Any]: + """Create a new bearer token with specified scopes and TTL.""" + payload = {"scopes": scopes, "ttl": ttl} + url = f"{self.url}/token" + return self.client.post(url=url, headers=self.headers, json=payload).json() + + def get_token(self, scope: str = None) -> Dict[str, Any]: + """Retrieve an existing token, optionally filtered by scope.""" + params = {"scope": scope or ""} + url = f"{self.url}/token" + return self.client.get(url=url, headers=self.headers, params=params).json() + + def delete_token(self, id: str) -> Dict[str, Any]: + """Revoke/Delete a specific token by ID.""" + url = f"{self.url}/token/{id}" + return self.client.delete(url=url, headers=self.headers).json() + + def get_counters(self, period: str, date: str) -> Dict[str, Any]: + """Retrieve usage counters for a specific period and date.""" + url = f"{self.url}/counters/{period}/{date}" + return self.client.get(url=url, headers=self.headers).json() From 71a228d354d1a582f1c4370adb5dded92e44b533 Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Tue, 31 Mar 2026 22:40:42 +0530 Subject: [PATCH 3/4] Update poetry.lock --- poetry.lock | 298 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 244 insertions(+), 54 deletions(-) diff --git a/poetry.lock b/poetry.lock index a2f6ff7..c5dbecb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,61 +1,105 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + [[package]] name = "anyio" -version = "3.6.2" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" +version = "4.13.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.10" +files = [ + {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"}, + {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"}, +] [package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" -sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16,<0.22)"] +trio = ["trio (>=0.32.0)"] [[package]] name = "certifi" -version = "2022.12.7" +version = "2026.2.25" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] [[package]] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] [[package]] name = "httpcore" -version = "0.17.0" +version = "0.17.3" description = "A minimal low-level HTTP client." -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, + {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, +] [package.dependencies] anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" +sniffio = "==1.*" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "httpx" -version = "0.24.0" +version = "0.24.1" description = "The next generation HTTP client." -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, + {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, +] [package.dependencies] certifi = "*" @@ -65,57 +109,203 @@ sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "idna" -version = "3.4" +version = "3.11" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -category = "main" +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] -[metadata] -lock-version = "1.1" -python-versions = "^3.10" -content-hash = "a660465356539cfe3153796ea737bd8e8c3d28c85befb9dd71dc2d92d9ed90d7" +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] -[metadata.files] -anyio = [ - {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, - {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] -certifi = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] -h11 = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] -httpcore = [ - {file = "httpcore-0.17.0-py3-none-any.whl", hash = "sha256:0fdfea45e94f0c9fd96eab9286077f9ff788dd186635ae61b312693e4d943599"}, - {file = "httpcore-0.17.0.tar.gz", hash = "sha256:cc045a3241afbf60ce056202301b4d8b6af08845e3294055eb26b09913ef903c"}, + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "ruff" +version = "0.4.10" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"}, + {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"}, + {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"}, + {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"}, + {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"}, + {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"}, ] -httpx = [ - {file = "httpx-0.24.0-py3-none-any.whl", hash = "sha256:447556b50c1921c351ea54b4fe79d91b724ed2b027462ab9a329465d147d5a4e"}, - {file = "httpx-0.24.0.tar.gz", hash = "sha256:507d676fc3e26110d41df7d35ebd8b3b8585052450f4097401c9be59d928c63e"}, + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + +[[package]] +name = "tomli" +version = "2.4.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, ] -sniffio = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "86c19a5b5fbff37939a046e2d03346f96c299b0239de4f6aadfe66d63c64b024" From b289eaaa2db2d27adcfed09a3c49d460f7311dab Mon Sep 17 00:00:00 2001 From: Deadpool2000 Date: Tue, 31 Mar 2026 22:47:40 +0530 Subject: [PATCH 4/4] fix: remove unused resp variable in tests and resolve ruff formatting issues --- openapi_python_sdk/client.py | 9 ++++----- tests/test_client.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/openapi_python_sdk/client.py b/openapi_python_sdk/client.py index 6d4cf1f..16956cf 100644 --- a/openapi_python_sdk/client.py +++ b/openapi_python_sdk/client.py @@ -4,9 +4,9 @@ import httpx # Backward compatibility imports -from .async_client import AsyncClient -from .async_oauth_client import AsyncOauthClient -from .oauth_client import OauthClient +from .async_client import AsyncClient # noqa: F401 +from .async_oauth_client import AsyncOauthClient # noqa: F401 +from .oauth_client import OauthClient # noqa: F401 class Client: @@ -61,5 +61,4 @@ def request( data = json.loads(data) except json.JSONDecodeError: pass - - return data \ No newline at end of file + return data diff --git a/tests/test_client.py b/tests/test_client.py index d9ac4dd..75fe1e4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -103,7 +103,7 @@ def test_defaults_on_empty_request(self, mock_httpx): mock_httpx.return_value.request.return_value = mock_resp client = Client(token="tok") - resp = client.request() + client.request() mock_httpx.return_value.request.assert_called_once_with( method="GET", url="", headers=client.headers, json={}, params={}