Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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="<your_username>", apikey="<your_apikey>", test=True)

Expand All @@ -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)

Expand All @@ -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="<your_username>", apikey="<your_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:
Expand Down Expand Up @@ -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/).

1 change: 1 addition & 0 deletions openapi-python-sdk
Submodule openapi-python-sdk added at 37018d
7 changes: 7 additions & 0 deletions openapi_python_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
174 changes: 168 additions & 6 deletions openapi_python_sdk/client.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}"
Expand All @@ -54,13 +141,28 @@ 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",
url: str = None,
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 ""
Expand All @@ -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
Loading
Loading