Skip to content

Commit 5637eba

Browse files
Merge pull request #14 from Deadpool2000/fix-issue-25-3-new
Add Async support with AsyncClient and AsyncOauthClient #5
2 parents 37018d7 + b289eaa commit 5637eba

File tree

10 files changed

+604
-103
lines changed

10 files changed

+604
-103
lines changed

README.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ Before using the Openapi Python Client, you will need an account at [Openapi](ht
2626
## Features
2727

2828
- **Agnostic Design**: No API-specific classes, works with any OpenAPI service
29-
- **Minimal Dependencies**: Only requires Python 3.8+ and `requests`
29+
- **Minimal Dependencies**: Only requires Python 3.8+ and `httpx`
3030
- **OAuth Support**: Built-in OAuth client for token management
3131
- **HTTP Primitives**: GET, POST, PUT, DELETE, PATCH methods
32+
- **Async Support**: Fully compatible with async frameworks like FastAPI and aiohttp
3233
- **Clean Interface**: Similar to the Rust SDK design
3334

3435
## What you can do
@@ -70,7 +71,7 @@ Interaction with the Openapi platform happens in two distinct steps.
7071
Authenticate with your credentials and obtain a short-lived bearer token scoped to the endpoints you need.
7172

7273
```python
73-
from openapi_python_sdk.client import OauthClient
74+
from openapi_python_sdk import OauthClient
7475

7576
oauth = OauthClient(username="<your_username>", apikey="<your_apikey>", test=True)
7677

@@ -92,7 +93,7 @@ oauth.delete_token(id=token)
9293
Use the token to make authenticated requests to any Openapi service.
9394

9495
```python
95-
from openapi_python_sdk.client import Client
96+
from openapi_python_sdk import Client
9697

9798
client = Client(token=token)
9899

@@ -111,6 +112,37 @@ resp = client.request(
111112
)
112113
```
113114

115+
## Async Usage
116+
117+
The SDK provides `AsyncClient` and `AsyncOauthClient` for use with asynchronous frameworks like FastAPI or `aiohttp`.
118+
119+
### Async Authentication
120+
121+
```python
122+
from openapi_python_sdk import AsyncOauthClient
123+
124+
async with AsyncOauthClient(username="<your_username>", apikey="<your_apikey>", test=True) as oauth:
125+
resp = await oauth.create_token(
126+
scopes=["GET:test.imprese.openapi.it/advance"],
127+
ttl=3600,
128+
)
129+
token = resp["token"]
130+
```
131+
132+
### Async Requests
133+
134+
```python
135+
from openapi_python_sdk import AsyncClient
136+
137+
async with AsyncClient(token=token) as client:
138+
resp = await client.request(
139+
method="GET",
140+
url="https://test.imprese.openapi.it/advance",
141+
params={"denominazione": "altravia"},
142+
)
143+
```
144+
145+
114146
## Testing
115147

116148
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
174206
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.
175207

176208
For more details, see the full license text at the [MIT License page](https://choosealicense.com/licenses/mit/).
177-

openapi-python-sdk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 37018d7aebbaa11312900e19b5a23c854553ee8f

openapi_python_sdk/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
Openapi Python SDK - A minimal and agnostic SDK for the Openapi marketplace.
3+
Exports both synchronous and asynchronous clients.
4+
"""
5+
from .async_client import AsyncClient
6+
from .async_oauth_client import AsyncOauthClient
7+
from .client import Client
8+
from .oauth_client import OauthClient
9+
10+
__all__ = ["Client", "AsyncClient", "OauthClient", "AsyncOauthClient"]

openapi_python_sdk/async_client.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import json
2+
from typing import Any, Dict
3+
4+
import httpx
5+
6+
7+
class AsyncClient:
8+
"""
9+
Asynchronous client for making authenticated requests to Openapi endpoints.
10+
Suitable for use with FastAPI, aiohttp, etc.
11+
"""
12+
13+
def __init__(self, token: str):
14+
self.client = httpx.AsyncClient()
15+
self.auth_header: str = f"Bearer {token}"
16+
self.headers: Dict[str, str] = {
17+
"Authorization": self.auth_header,
18+
"Content-Type": "application/json",
19+
}
20+
21+
async def __aenter__(self):
22+
"""Enable use as an asynchronous context manager."""
23+
return self
24+
25+
async def __aexit__(self, exc_type, exc_val, exc_tb):
26+
"""Ensure the underlying HTTP client is closed on exit (async)."""
27+
await self.client.aclose()
28+
29+
async def aclose(self):
30+
"""Manually close the underlying HTTP client (async)."""
31+
await self.client.aclose()
32+
33+
async def request(
34+
self,
35+
method: str = "GET",
36+
url: str = None,
37+
payload: Dict[str, Any] = None,
38+
params: Dict[str, Any] = None,
39+
) -> Dict[str, Any]:
40+
"""
41+
Make an asynchronous HTTP request to the specified Openapi endpoint.
42+
"""
43+
payload = payload or {}
44+
params = params or {}
45+
url = url or ""
46+
resp = await self.client.request(
47+
method=method,
48+
url=url,
49+
headers=self.headers,
50+
json=payload,
51+
params=params,
52+
)
53+
data = resp.json()
54+
55+
# Handle cases where the API might return a JSON-encoded string instead of an object
56+
if isinstance(data, str):
57+
try:
58+
data = json.loads(data)
59+
except json.JSONDecodeError:
60+
pass
61+
62+
return data
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import base64
2+
from typing import Any, Dict, List
3+
4+
import httpx
5+
6+
from .oauth_client import OAUTH_BASE_URL, TEST_OAUTH_BASE_URL
7+
8+
9+
class AsyncOauthClient:
10+
"""
11+
Asynchronous client for handling Openapi authentication and token management.
12+
Suitable for use with FastAPI, aiohttp, etc.
13+
"""
14+
15+
def __init__(self, username: str, apikey: str, test: bool = False):
16+
self.client = httpx.AsyncClient()
17+
self.url: str = TEST_OAUTH_BASE_URL if test else OAUTH_BASE_URL
18+
self.auth_header: str = (
19+
"Basic " + base64.b64encode(f"{username}:{apikey}".encode("utf-8")).decode()
20+
)
21+
self.headers: Dict[str, Any] = {
22+
"Authorization": self.auth_header,
23+
"Content-Type": "application/json",
24+
}
25+
26+
async def __aenter__(self):
27+
"""Enable use as an asynchronous context manager."""
28+
return self
29+
30+
async def __aexit__(self, exc_type, exc_val, exc_tb):
31+
"""Ensure the underlying HTTP client is closed on exit (async)."""
32+
await self.client.aclose()
33+
34+
async def aclose(self):
35+
"""Manually close the underlying HTTP client (async)."""
36+
await self.client.aclose()
37+
38+
async def get_scopes(self, limit: bool = False) -> Dict[str, Any]:
39+
"""Retrieve available scopes for the current user (async)."""
40+
params = {"limit": int(limit)}
41+
url = f"{self.url}/scopes"
42+
resp = await self.client.get(url=url, headers=self.headers, params=params)
43+
return resp.json()
44+
45+
async def create_token(self, scopes: List[str] = [], ttl: int = 0) -> Dict[str, Any]:
46+
"""Create a new bearer token with specified scopes and TTL (async)."""
47+
payload = {"scopes": scopes, "ttl": ttl}
48+
url = f"{self.url}/token"
49+
resp = await self.client.post(url=url, headers=self.headers, json=payload)
50+
return resp.json()
51+
52+
async def get_token(self, scope: str = None) -> Dict[str, Any]:
53+
"""Retrieve an existing token, optionally filtered by scope (async)."""
54+
params = {"scope": scope or ""}
55+
url = f"{self.url}/token"
56+
resp = await self.client.get(url=url, headers=self.headers, params=params)
57+
return resp.json()
58+
59+
async def delete_token(self, id: str) -> Dict[str, Any]:
60+
"""Revoke/Delete a specific token by ID (async)."""
61+
url = f"{self.url}/token/{id}"
62+
resp = await self.client.delete(url=url, headers=self.headers)
63+
return resp.json()
64+
65+
async def get_counters(self, period: str, date: str) -> Dict[str, Any]:
66+
"""Retrieve usage counters for a specific period and date (async)."""
67+
url = f"{self.url}/counters/{period}/{date}"
68+
resp = await self.client.get(url=url, headers=self.headers)
69+
return resp.json()

openapi_python_sdk/client.py

Lines changed: 28 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,19 @@
1-
import base64
1+
import json
22
from typing import Any, Dict
33

44
import httpx
55

6-
import json
7-
8-
OAUTH_BASE_URL = "https://oauth.openapi.it"
9-
TEST_OAUTH_BASE_URL = "https://test.oauth.openapi.it"
10-
11-
12-
class OauthClient:
13-
def __init__(self, username: str, apikey: str, test: bool = False):
14-
self.client = httpx.Client()
15-
self.url: str = TEST_OAUTH_BASE_URL if test else OAUTH_BASE_URL
16-
self.auth_header: str = (
17-
"Basic " + base64.b64encode(f"{username}:{apikey}".encode("utf-8")).decode()
18-
)
19-
self.headers: Dict[str, Any] = {
20-
"Authorization": self.auth_header,
21-
"Content-Type": "application/json",
22-
}
23-
24-
def get_scopes(self, limit: bool = False) -> Dict[str, Any]:
25-
params = {"limit": int(limit)}
26-
url = f"{self.url}/scopes"
27-
return self.client.get(url=url, headers=self.headers, params=params).json()
28-
29-
def create_token(self, scopes: list[str] = [], ttl: int = 0) -> Dict[str, Any]:
30-
payload = {"scopes": scopes, "ttl": ttl}
31-
url = f"{self.url}/token"
32-
return self.client.post(url=url, headers=self.headers, json=payload).json()
33-
34-
def get_token(self, scope: str = None) -> Dict[str, Any]:
35-
params = {"scope": scope or ""}
36-
url = f"{self.url}/token"
37-
return self.client.get(url=url, headers=self.headers, params=params).json()
38-
39-
def delete_token(self, id: str) -> Dict[str, Any]:
40-
url = f"{self.url}/token/{id}"
41-
return self.client.delete(url=url, headers=self.headers).json()
42-
43-
def get_counters(self, period: str, date: str) -> Dict[str, Any]:
44-
url = f"{self.url}/counters/{period}/{date}"
45-
return self.client.get(url=url, headers=self.headers).json()
6+
# Backward compatibility imports
7+
from .async_client import AsyncClient # noqa: F401
8+
from .async_oauth_client import AsyncOauthClient # noqa: F401
9+
from .oauth_client import OauthClient # noqa: F401
4610

4711

4812
class Client:
13+
"""
14+
Synchronous client for making authenticated requests to Openapi endpoints.
15+
"""
16+
4917
def __init__(self, token: str):
5018
self.client = httpx.Client()
5119
self.auth_header: str = f"Bearer {token}"
@@ -54,13 +22,28 @@ def __init__(self, token: str):
5422
"Content-Type": "application/json",
5523
}
5624

25+
def __enter__(self):
26+
"""Enable use as a synchronous context manager."""
27+
return self
28+
29+
def __exit__(self, exc_type, exc_val, exc_tb):
30+
"""Ensure the underlying HTTP client is closed on exit."""
31+
self.client.close()
32+
33+
def close(self):
34+
"""Manually close the underlying HTTP client."""
35+
self.client.close()
36+
5737
def request(
5838
self,
5939
method: str = "GET",
6040
url: str = None,
6141
payload: Dict[str, Any] = None,
6242
params: Dict[str, Any] = None,
6343
) -> Dict[str, Any]:
44+
"""
45+
Make a synchronous HTTP request to the specified Openapi endpoint.
46+
"""
6447
payload = payload or {}
6548
params = params or {}
6649
url = url or ""
@@ -72,9 +55,10 @@ def request(
7255
params=params,
7356
).json()
7457

75-
if isinstance(data,str):
58+
# Handle cases where the API might return a JSON-encoded string instead of an object
59+
if isinstance(data, str):
7660
try:
77-
data=json.loads(data)
61+
data = json.loads(data)
7862
except json.JSONDecodeError:
7963
pass
80-
return data
64+
return data

openapi_python_sdk/oauth_client.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import base64
2+
from typing import Any, Dict, List
3+
4+
import httpx
5+
6+
OAUTH_BASE_URL = "https://oauth.openapi.it"
7+
TEST_OAUTH_BASE_URL = "https://test.oauth.openapi.it"
8+
9+
10+
class OauthClient:
11+
"""
12+
Synchronous client for handling Openapi authentication and token management.
13+
"""
14+
15+
def __init__(self, username: str, apikey: str, test: bool = False):
16+
self.client = httpx.Client()
17+
self.url: str = TEST_OAUTH_BASE_URL if test else OAUTH_BASE_URL
18+
self.auth_header: str = (
19+
"Basic " + base64.b64encode(f"{username}:{apikey}".encode("utf-8")).decode()
20+
)
21+
self.headers: Dict[str, Any] = {
22+
"Authorization": self.auth_header,
23+
"Content-Type": "application/json",
24+
}
25+
26+
def __enter__(self):
27+
"""Enable use as a synchronous context manager."""
28+
return self
29+
30+
def __exit__(self, exc_type, exc_val, exc_tb):
31+
"""Ensure the underlying HTTP client is closed on exit."""
32+
self.client.close()
33+
34+
def close(self):
35+
"""Manually close the underlying HTTP client."""
36+
self.client.close()
37+
38+
def get_scopes(self, limit: bool = False) -> Dict[str, Any]:
39+
"""Retrieve available scopes for the current user."""
40+
params = {"limit": int(limit)}
41+
url = f"{self.url}/scopes"
42+
return self.client.get(url=url, headers=self.headers, params=params).json()
43+
44+
def create_token(self, scopes: List[str] = [], ttl: int = 0) -> Dict[str, Any]:
45+
"""Create a new bearer token with specified scopes and TTL."""
46+
payload = {"scopes": scopes, "ttl": ttl}
47+
url = f"{self.url}/token"
48+
return self.client.post(url=url, headers=self.headers, json=payload).json()
49+
50+
def get_token(self, scope: str = None) -> Dict[str, Any]:
51+
"""Retrieve an existing token, optionally filtered by scope."""
52+
params = {"scope": scope or ""}
53+
url = f"{self.url}/token"
54+
return self.client.get(url=url, headers=self.headers, params=params).json()
55+
56+
def delete_token(self, id: str) -> Dict[str, Any]:
57+
"""Revoke/Delete a specific token by ID."""
58+
url = f"{self.url}/token/{id}"
59+
return self.client.delete(url=url, headers=self.headers).json()
60+
61+
def get_counters(self, period: str, date: str) -> Dict[str, Any]:
62+
"""Retrieve usage counters for a specific period and date."""
63+
url = f"{self.url}/counters/{period}/{date}"
64+
return self.client.get(url=url, headers=self.headers).json()

0 commit comments

Comments
 (0)