Metadata-Version: 2.1
Name: subnoto-api-client
Author: Subnoto
Author-email: support@subnoto.com
Home-page: https://subnoto.com
License: Apache-2.0
Description-Content-Type: text/markdown
Summary: Python client for the Subnoto Public API
Project-URL: Documentation, https://subnoto.com/documentation/developers/sdks/python
Project-URL: Homepage, https://subnoto.com
Project-URL: Repository, https://gitlab.com/subnoto/subnoto-monorepo-public
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Requires-Python: >=3.11
Requires-Dist: httpx
Requires-Dist: attrs
Requires-Dist: http-message-signatures
Requires-Dist: typer
Requires-Dist: typing_extensions
Version: 2.13.4

# Subnoto Python SDK

Python client for the Subnoto Public API.

## Table of contents

- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [Error handling and tunnel retry](#error-handling-and-tunnel-retry)
- [Documentation](#documentation)

## Installation

**Requirements:** Python 3.9+

Install from PyPI:

```bash
pip install subnoto-api-client
```

## Usage

The SDK provides both async and sync clients. Use `SubnotoClient` for async/await code
or `SubnotoSyncClient` for synchronous code. Credentials are required; you can pass
them explicitly or set `SUBNOTO_ACCESS_KEY` and `SUBNOTO_SECRET_KEY`.

### Async Example

```python
import asyncio
from subnoto_api_client import SubnotoClient, SubnotoConfig

async def main():
    async with SubnotoClient(SubnotoConfig()) as client:
        response = await client.post("/public/workspace/list", json={})
        print(f"Workspaces: {response.json()}")

if __name__ == "__main__":
    asyncio.run(main())
```

### Sync Example

```python
from subnoto_api_client import SubnotoSyncClient, SubnotoConfig

with SubnotoSyncClient(SubnotoConfig()) as client:
    response = client.post("/public/workspace/list", json={})
    print(f"Workspaces: {response.json()}")
```

## Configuration

| Option       | Type | Required | Description                                                       |
| ------------ | ---- | -------- | ----------------------------------------------------------------- |
| `access_key` | str  | Yes      | API access key (or set env var `SUBNOTO_ACCESS_KEY`)              |
| `secret_key` | str  | Yes      | API secret key, hex-encoded (or set env var `SUBNOTO_SECRET_KEY`) |

## Error handling and tunnel retry

The SDK follows the [Subnoto error reference](https://subnoto.com/documentation/developers/reference/errors).
On tunnel 401 errors (`TUNNEL_SESSION_NOT_FOUND` or `TUNNEL_ERROR`), the client automatically
invalidates the tunnel session and **retries the request up to 3 times** with a new session.
You do not need to manage tunnel lifecycle when using `SubnotoClient` or `SubnotoSyncClient`.

For custom handling you can use the exported helpers and codes. You can raise
`SubnotoError(message, response.status_code)` with the response body message and status code.

```python
from subnoto_api_client import (
    SubnotoClient,
    SubnotoConfig,
    get_error_code,
    is_tunnel_error,
    SubnotoError,
    TUNNEL_SESSION_NOT_FOUND,
    TUNNEL_ERROR,
)

async def main():
    async with SubnotoClient(SubnotoConfig()) as client:
        response = await client.post("/public/utils/whoami", json={})
        if response.status_code != 200:
            body = response.json()
            code = get_error_code(body)  # works with API shape (error.code) and top-level code
            if is_tunnel_error(body):
                # SDK already retried (up to 3 times); this is the result after retries
                pass
            msg = body.get("error", {}).get("message", str(body)) if isinstance(body, dict) else str(body)
            raise SubnotoError(msg, response.status_code)
```

See the [Subnoto error reference](https://subnoto.com/documentation/developers/reference/errors) for all error codes.

## Documentation

The API documentation is [available here](https://subnoto.com/documentation/developers/openapi).

