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
10 changes: 10 additions & 0 deletions openapi_python_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Openapi Python SDK - A minimal and agnostic SDK for the Openapi marketplace.
Exports both synchronous and asynchronous clients.
"""
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"]
62 changes: 62 additions & 0 deletions openapi_python_sdk/async_client.py
Original file line number Diff line number Diff line change
@@ -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
69 changes: 69 additions & 0 deletions openapi_python_sdk/async_oauth_client.py
Original file line number Diff line number Diff line change
@@ -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()
72 changes: 28 additions & 44 deletions openapi_python_sdk/client.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,19 @@
import base64
import json
from typing import Any, Dict

import httpx

import json

OAUTH_BASE_URL = "https://oauth.openapi.it"
TEST_OAUTH_BASE_URL = "https://test.oauth.openapi.it"


class OauthClient:
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 get_scopes(self, limit: bool = False) -> Dict[str, Any]:
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]:
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]:
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]:
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]:
url = f"{self.url}/counters/{period}/{date}"
return self.client.get(url=url, headers=self.headers).json()
# Backward compatibility imports
from .async_client import AsyncClient # noqa: F401
from .async_oauth_client import AsyncOauthClient # noqa: F401
from .oauth_client import OauthClient # noqa: F401


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 +22,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 +55,10 @@ 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)
data = json.loads(data)
except json.JSONDecodeError:
pass
return data
return data
64 changes: 64 additions & 0 deletions openapi_python_sdk/oauth_client.py
Original file line number Diff line number Diff line change
@@ -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()
Loading
Loading