tovano 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tovano-1.0.0/.gitignore +9 -0
- tovano-1.0.0/CHANGELOG.md +14 -0
- tovano-1.0.0/LICENSE +21 -0
- tovano-1.0.0/PKG-INFO +245 -0
- tovano-1.0.0/README.md +215 -0
- tovano-1.0.0/pyproject.toml +67 -0
- tovano-1.0.0/src/tovano/__init__.py +98 -0
- tovano-1.0.0/src/tovano/_base.py +135 -0
- tovano-1.0.0/src/tovano/_client.py +693 -0
- tovano-1.0.0/src/tovano/_errors.py +242 -0
- tovano-1.0.0/src/tovano/_money.py +33 -0
- tovano-1.0.0/src/tovano/_streaming.py +207 -0
- tovano-1.0.0/src/tovano/_types.py +333 -0
- tovano-1.0.0/src/tovano/_version.py +1 -0
- tovano-1.0.0/src/tovano/_webhooks.py +101 -0
- tovano-1.0.0/src/tovano/py.typed +0 -0
- tovano-1.0.0/tests/conftest.py +105 -0
- tovano-1.0.0/tests/test_integration.py +298 -0
- tovano-1.0.0/tests/test_unit.py +450 -0
tovano-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 (2026-11-16)
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- `Tovano` (sync) and `AsyncTovano` (asyncio) clients with the full `/v1` API: models, runs (create, get, list, iterate, wait, run), chat completions with streaming, balance, usage.
|
|
8
|
+
- Default base URL `https://api.tovano.ai`.
|
|
9
|
+
- Typed response objects (dicts with attribute access) and typed errors, including `InsufficientCreditsError` and `SpendLimitReachedError` (402) and `RateLimitError` with `retry_after`.
|
|
10
|
+
- Retries with exponential backoff and jitter, `Retry-After` honoured, the same `Idempotency-Key` on every attempt.
|
|
11
|
+
- Customer tags per call or per client (`with_options`).
|
|
12
|
+
- Webhook verification with timestamp tolerance and multiple signatures; `webhooks.sign()` for tests.
|
|
13
|
+
- Python 3.9 to 3.14. One dependency: `httpx`. Fully typed (`py.typed`, mypy strict).
|
|
14
|
+
- `Balance.balance_credits` and usage `succeeded` / `all` counts.
|
tovano-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tovano
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tovano-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tovano
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python client for the Tovano API: one API key, one balance and one invoice for image, video, audio and text models.
|
|
5
|
+
Project-URL: Homepage, https://tovano.ai
|
|
6
|
+
Project-URL: Documentation, https://tovano.ai/docs/sdk
|
|
7
|
+
Project-URL: Repository, https://github.com/tovanoai/tovano-python
|
|
8
|
+
Project-URL: Issues, https://github.com/tovanoai/tovano-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/tovanoai/tovano-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Tovano <support@tovano.ai>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ai,api,image generation,llm,openai compatible,sdk,text to speech,tovano,video generation
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Requires-Dist: httpx<1,>=0.25
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# tovano
|
|
32
|
+
|
|
33
|
+
The official Python client for [Tovano](https://tovano.ai): one API key, one prepaid balance and one invoice for image, video, audio and text models, charged per unit.
|
|
34
|
+
|
|
35
|
+
| | |
|
|
36
|
+
|---|---|
|
|
37
|
+
| Python | 3.9 to 3.14 |
|
|
38
|
+
| Clients | `Tovano` (sync) and `AsyncTovano` (asyncio) |
|
|
39
|
+
| Dependencies | `httpx` only |
|
|
40
|
+
| Built in | Retries with backoff, idempotency keys, streaming, typed errors and objects, webhook verification |
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install "git+https://github.com/tovanoai/tovano-python"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv add tovano
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or straight from GitHub:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install git+https://github.com/tovanoai/tovano-python
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from tovano import Tovano, format_usd
|
|
62
|
+
|
|
63
|
+
client = Tovano() # reads TOVANO_API_KEY
|
|
64
|
+
|
|
65
|
+
run = client.runs.run(
|
|
66
|
+
model="image/flux-1.1-pro",
|
|
67
|
+
input={"prompt": "Product photo of a blue ceramic mug", "n": 2},
|
|
68
|
+
)
|
|
69
|
+
print(run.status, run.units, format_usd(run.charged_micro)) # completed 2 $0.096
|
|
70
|
+
print(run.output.images[0].url)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Money is always an integer number of micro-dollars (1 USD = 1_000_000) in fields ending in `_micro`. `format_usd` turns it into text; `usd_to_micro` and `micro_to_usd` convert exactly (with `Decimal`).
|
|
74
|
+
|
|
75
|
+
Responses are typed objects that are also dicts: `run.status` and `run["status"]` both work, and `json.dumps(run)` works.
|
|
76
|
+
|
|
77
|
+
## Try it without a key
|
|
78
|
+
|
|
79
|
+
Every example in [`examples/`](./examples) runs in **offline mode** when `TOVANO_API_KEY` is not set: a simulated API answers with the same shapes and prices, at no cost.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git clone https://github.com/tovanoai/tovano-python && cd tovano-python && uv sync
|
|
83
|
+
uv run python examples/quickstart.py
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| Example | Shows |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `quickstart.py` | Balance, an image run, charge |
|
|
89
|
+
| `models.py` | The catalogue with prices |
|
|
90
|
+
| `chat.py` / `chat_stream.py` | OpenAI-compatible chat, with streaming |
|
|
91
|
+
| `async_batch.py` | Several models at once with asyncio |
|
|
92
|
+
| `agent_job.py` | An agent job with one charge per step |
|
|
93
|
+
| `customer_usage.py` | Customer tags and spend per customer |
|
|
94
|
+
| `errors.py` | Typed errors, retries, failed runs |
|
|
95
|
+
| `webhook_server.py` | Receive and verify webhooks (standard library) |
|
|
96
|
+
|
|
97
|
+
## Client options
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
client = Tovano(
|
|
101
|
+
api_key="tvn_live_...", # default: TOVANO_API_KEY
|
|
102
|
+
base_url="https://api.tovano.ai", # default: TOVANO_BASE_URL, else https://api.tovano.ai
|
|
103
|
+
max_retries=2, # connection errors, timeouts, 408, 429, 5xx
|
|
104
|
+
timeout=60.0, # seconds per attempt
|
|
105
|
+
customer="acme", # default customer tag for every call
|
|
106
|
+
webhook_secret="whsec_...", # default: TOVANO_WEBHOOK_SECRET
|
|
107
|
+
app_info="my-app/1.2.0",
|
|
108
|
+
http_client=None, # your own httpx.Client (proxies, transports)
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Every method also takes per-call keyword options: `idempotency_key`, `customer` (header), `extra_headers`, `timeout`, `max_retries`. Use `with Tovano() as client:` or `client.close()` to free connections.
|
|
113
|
+
|
|
114
|
+
## Async
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import asyncio
|
|
118
|
+
from tovano import AsyncTovano
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def main():
|
|
122
|
+
async with AsyncTovano() as client:
|
|
123
|
+
a, b = await asyncio.gather(
|
|
124
|
+
client.runs.run(model="image/flux-1.1-pro", input={"prompt": "Mug"}),
|
|
125
|
+
client.runs.run(model="audio/eleven-multilingual-v2", input={"text": "Hello"}),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
asyncio.run(main())
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`AsyncTovano` has the same methods as `Tovano`, as coroutines.
|
|
133
|
+
|
|
134
|
+
## Runs
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
run = client.runs.create(
|
|
138
|
+
model="video/kling-2.1-pro",
|
|
139
|
+
input={"prompt": "Waves on a beach", "duration": 5},
|
|
140
|
+
webhook_url="https://example.com/tovano-webhook", # optional
|
|
141
|
+
)
|
|
142
|
+
done = client.runs.wait(run.id, timeout=120)
|
|
143
|
+
if done.status == "failed":
|
|
144
|
+
print(done.error.message, "(not charged)")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
| Method | What it does |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `runs.create(model=, input=, customer=, webhook_url=)` | Starts a run. POSTs get a random `Idempotency-Key`, so a retry never runs twice. |
|
|
150
|
+
| `runs.get(run_id)` | Status, output, units and charge |
|
|
151
|
+
| `runs.wait(run_id, interval=0.5, timeout=600)` | Polls until done. Raises `RunTimeoutError`. |
|
|
152
|
+
| `runs.run(..., raise_on_failure=False)` | `create` + `wait` |
|
|
153
|
+
| `runs.list(status=, model=, customer=, type=, limit=, cursor=)` | One `Page` (iterable), newest first |
|
|
154
|
+
| `runs.iterate(**filters)` | Every match, paging for you |
|
|
155
|
+
|
|
156
|
+
## Chat (OpenAI-compatible)
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
res = client.chat.completions.create(
|
|
160
|
+
model="text/gpt-4.1-mini",
|
|
161
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
162
|
+
)
|
|
163
|
+
print(res.choices[0].message.content, res.usage.total_tokens, res.tovano.charged_micro)
|
|
164
|
+
|
|
165
|
+
with client.chat.completions.create(model="text/gpt-4.1-mini", messages=messages, stream=True) as stream:
|
|
166
|
+
for chunk in stream:
|
|
167
|
+
if chunk.choices:
|
|
168
|
+
print(chunk.choices[0].delta.content or "", end="")
|
|
169
|
+
result = stream.final_result() # content, finish_reason, usage, tovano
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Leaving the `with` block early stops the stream; tokens already produced are charged. `stream.run_id` is the run that records the call. Async: `async for chunk in await client.chat.completions.create(..., stream=True)`.
|
|
173
|
+
|
|
174
|
+
Any OpenAI client works too: set its base URL to `https://api.tovano.ai/v1` and use your Tovano key.
|
|
175
|
+
|
|
176
|
+
## Models, balance and usage
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
models = client.models.list() # type, unit, prices
|
|
180
|
+
model = client.models.get("image/flux-1.1-pro") # plus input_schema (JSON Schema)
|
|
181
|
+
balance = client.balance() # available_micro, reserved_micro, spent_this_month_micro
|
|
182
|
+
usage = client.usage(group_by="customer", from_="2026-11-01")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`group_by` is `model`, `customer`, `day`, `project` or `key`. `from_` has an underscore because `from` is a Python keyword.
|
|
186
|
+
|
|
187
|
+
## Customer tags
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
client.runs.create(model=model, input=inp, customer="acme") # on a run or chat call
|
|
191
|
+
client.balance(customer="acme") # any call, as an option
|
|
192
|
+
acme = client.with_options(customer="acme") # a client that tags every call
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Webhooks
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from tovano import webhooks, WebhookVerificationError
|
|
199
|
+
|
|
200
|
+
run = webhooks.construct_event(
|
|
201
|
+
raw_body, # the raw bytes, as received
|
|
202
|
+
request.headers["Tovano-Signature"],
|
|
203
|
+
os.environ["TOVANO_WEBHOOK_SECRET"],
|
|
204
|
+
)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The header is `t=<unix>,v1=<hex HMAC-SHA256 of "t.body">`. Signatures older than 5 minutes are rejected (`tolerance=` changes it). `webhooks.verify()` returns a bool; `webhooks.sign()` builds a header for your tests. `client.webhooks.*` uses the client's `webhook_secret`.
|
|
208
|
+
|
|
209
|
+
## Errors
|
|
210
|
+
|
|
211
|
+
Every failed call raises a `TovanoError` with `status`, `code`, `type`, `param`, `request_id` and `headers`.
|
|
212
|
+
|
|
213
|
+
| Class | Status | Code | Meaning |
|
|
214
|
+
|---|---|---|---|
|
|
215
|
+
| `AuthenticationError` | 401 | `invalid_api_key` | Missing, invalid or revoked key |
|
|
216
|
+
| `InsufficientCreditsError` | 402 | `insufficient_credits` | Add credits in the dashboard |
|
|
217
|
+
| `SpendLimitReachedError` | 402 | `spend_limit_reached` | Monthly spend limit reached |
|
|
218
|
+
| `PermissionDeniedError` | 403 | `forbidden` | |
|
|
219
|
+
| `NotFoundError` | 404 | `not_found` | Unknown model or run |
|
|
220
|
+
| `ConflictError` | 409 | `conflict` | Idempotency-Key reused with another body |
|
|
221
|
+
| `InvalidRequestError` | 422 | `invalid_request` | `param` names the field |
|
|
222
|
+
| `RateLimitError` | 429 | `rate_limited` | Retried for you; `retry_after` in seconds |
|
|
223
|
+
| `InternalServerError` | 5xx | `internal_error` | Retried for you |
|
|
224
|
+
| `ProviderError` | 502 | `provider_error` | The model provider failed; not charged |
|
|
225
|
+
| `APIConnectionError` / `APITimeoutError` | 0 | | No response |
|
|
226
|
+
| `RunTimeoutError` | | `run_timeout` | `runs.wait()` gave up; the run keeps running |
|
|
227
|
+
| `RunFailedError` | | | `runs.run(raise_on_failure=True)` |
|
|
228
|
+
|
|
229
|
+
`InsufficientCreditsError` and `SpendLimitReachedError` both subclass `PaymentRequiredError`.
|
|
230
|
+
|
|
231
|
+
**Retries:** connection errors, timeouts, 408, 429 and 5xx are retried `max_retries` times with exponential backoff and jitter. `Retry-After` is honoured up to 60 seconds. The same `Idempotency-Key` is sent on every attempt.
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
uv sync
|
|
237
|
+
uv run pytest # unit tests, plus integration tests against a local Tovano server when available
|
|
238
|
+
uv run mypy
|
|
239
|
+
uv run ruff check .
|
|
240
|
+
uv build
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT
|
tovano-1.0.0/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# tovano
|
|
2
|
+
|
|
3
|
+
The official Python client for [Tovano](https://tovano.ai): one API key, one prepaid balance and one invoice for image, video, audio and text models, charged per unit.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Python | 3.9 to 3.14 |
|
|
8
|
+
| Clients | `Tovano` (sync) and `AsyncTovano` (asyncio) |
|
|
9
|
+
| Dependencies | `httpx` only |
|
|
10
|
+
| Built in | Retries with backoff, idempotency keys, streaming, typed errors and objects, webhook verification |
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install "git+https://github.com/tovanoai/tovano-python"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv add tovano
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or straight from GitHub:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install git+https://github.com/tovanoai/tovano-python
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from tovano import Tovano, format_usd
|
|
32
|
+
|
|
33
|
+
client = Tovano() # reads TOVANO_API_KEY
|
|
34
|
+
|
|
35
|
+
run = client.runs.run(
|
|
36
|
+
model="image/flux-1.1-pro",
|
|
37
|
+
input={"prompt": "Product photo of a blue ceramic mug", "n": 2},
|
|
38
|
+
)
|
|
39
|
+
print(run.status, run.units, format_usd(run.charged_micro)) # completed 2 $0.096
|
|
40
|
+
print(run.output.images[0].url)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Money is always an integer number of micro-dollars (1 USD = 1_000_000) in fields ending in `_micro`. `format_usd` turns it into text; `usd_to_micro` and `micro_to_usd` convert exactly (with `Decimal`).
|
|
44
|
+
|
|
45
|
+
Responses are typed objects that are also dicts: `run.status` and `run["status"]` both work, and `json.dumps(run)` works.
|
|
46
|
+
|
|
47
|
+
## Try it without a key
|
|
48
|
+
|
|
49
|
+
Every example in [`examples/`](./examples) runs in **offline mode** when `TOVANO_API_KEY` is not set: a simulated API answers with the same shapes and prices, at no cost.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/tovanoai/tovano-python && cd tovano-python && uv sync
|
|
53
|
+
uv run python examples/quickstart.py
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Example | Shows |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `quickstart.py` | Balance, an image run, charge |
|
|
59
|
+
| `models.py` | The catalogue with prices |
|
|
60
|
+
| `chat.py` / `chat_stream.py` | OpenAI-compatible chat, with streaming |
|
|
61
|
+
| `async_batch.py` | Several models at once with asyncio |
|
|
62
|
+
| `agent_job.py` | An agent job with one charge per step |
|
|
63
|
+
| `customer_usage.py` | Customer tags and spend per customer |
|
|
64
|
+
| `errors.py` | Typed errors, retries, failed runs |
|
|
65
|
+
| `webhook_server.py` | Receive and verify webhooks (standard library) |
|
|
66
|
+
|
|
67
|
+
## Client options
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
client = Tovano(
|
|
71
|
+
api_key="tvn_live_...", # default: TOVANO_API_KEY
|
|
72
|
+
base_url="https://api.tovano.ai", # default: TOVANO_BASE_URL, else https://api.tovano.ai
|
|
73
|
+
max_retries=2, # connection errors, timeouts, 408, 429, 5xx
|
|
74
|
+
timeout=60.0, # seconds per attempt
|
|
75
|
+
customer="acme", # default customer tag for every call
|
|
76
|
+
webhook_secret="whsec_...", # default: TOVANO_WEBHOOK_SECRET
|
|
77
|
+
app_info="my-app/1.2.0",
|
|
78
|
+
http_client=None, # your own httpx.Client (proxies, transports)
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Every method also takes per-call keyword options: `idempotency_key`, `customer` (header), `extra_headers`, `timeout`, `max_retries`. Use `with Tovano() as client:` or `client.close()` to free connections.
|
|
83
|
+
|
|
84
|
+
## Async
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import asyncio
|
|
88
|
+
from tovano import AsyncTovano
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def main():
|
|
92
|
+
async with AsyncTovano() as client:
|
|
93
|
+
a, b = await asyncio.gather(
|
|
94
|
+
client.runs.run(model="image/flux-1.1-pro", input={"prompt": "Mug"}),
|
|
95
|
+
client.runs.run(model="audio/eleven-multilingual-v2", input={"text": "Hello"}),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
asyncio.run(main())
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`AsyncTovano` has the same methods as `Tovano`, as coroutines.
|
|
103
|
+
|
|
104
|
+
## Runs
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
run = client.runs.create(
|
|
108
|
+
model="video/kling-2.1-pro",
|
|
109
|
+
input={"prompt": "Waves on a beach", "duration": 5},
|
|
110
|
+
webhook_url="https://example.com/tovano-webhook", # optional
|
|
111
|
+
)
|
|
112
|
+
done = client.runs.wait(run.id, timeout=120)
|
|
113
|
+
if done.status == "failed":
|
|
114
|
+
print(done.error.message, "(not charged)")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Method | What it does |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `runs.create(model=, input=, customer=, webhook_url=)` | Starts a run. POSTs get a random `Idempotency-Key`, so a retry never runs twice. |
|
|
120
|
+
| `runs.get(run_id)` | Status, output, units and charge |
|
|
121
|
+
| `runs.wait(run_id, interval=0.5, timeout=600)` | Polls until done. Raises `RunTimeoutError`. |
|
|
122
|
+
| `runs.run(..., raise_on_failure=False)` | `create` + `wait` |
|
|
123
|
+
| `runs.list(status=, model=, customer=, type=, limit=, cursor=)` | One `Page` (iterable), newest first |
|
|
124
|
+
| `runs.iterate(**filters)` | Every match, paging for you |
|
|
125
|
+
|
|
126
|
+
## Chat (OpenAI-compatible)
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
res = client.chat.completions.create(
|
|
130
|
+
model="text/gpt-4.1-mini",
|
|
131
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
132
|
+
)
|
|
133
|
+
print(res.choices[0].message.content, res.usage.total_tokens, res.tovano.charged_micro)
|
|
134
|
+
|
|
135
|
+
with client.chat.completions.create(model="text/gpt-4.1-mini", messages=messages, stream=True) as stream:
|
|
136
|
+
for chunk in stream:
|
|
137
|
+
if chunk.choices:
|
|
138
|
+
print(chunk.choices[0].delta.content or "", end="")
|
|
139
|
+
result = stream.final_result() # content, finish_reason, usage, tovano
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Leaving the `with` block early stops the stream; tokens already produced are charged. `stream.run_id` is the run that records the call. Async: `async for chunk in await client.chat.completions.create(..., stream=True)`.
|
|
143
|
+
|
|
144
|
+
Any OpenAI client works too: set its base URL to `https://api.tovano.ai/v1` and use your Tovano key.
|
|
145
|
+
|
|
146
|
+
## Models, balance and usage
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
models = client.models.list() # type, unit, prices
|
|
150
|
+
model = client.models.get("image/flux-1.1-pro") # plus input_schema (JSON Schema)
|
|
151
|
+
balance = client.balance() # available_micro, reserved_micro, spent_this_month_micro
|
|
152
|
+
usage = client.usage(group_by="customer", from_="2026-11-01")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`group_by` is `model`, `customer`, `day`, `project` or `key`. `from_` has an underscore because `from` is a Python keyword.
|
|
156
|
+
|
|
157
|
+
## Customer tags
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
client.runs.create(model=model, input=inp, customer="acme") # on a run or chat call
|
|
161
|
+
client.balance(customer="acme") # any call, as an option
|
|
162
|
+
acme = client.with_options(customer="acme") # a client that tags every call
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Webhooks
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from tovano import webhooks, WebhookVerificationError
|
|
169
|
+
|
|
170
|
+
run = webhooks.construct_event(
|
|
171
|
+
raw_body, # the raw bytes, as received
|
|
172
|
+
request.headers["Tovano-Signature"],
|
|
173
|
+
os.environ["TOVANO_WEBHOOK_SECRET"],
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The header is `t=<unix>,v1=<hex HMAC-SHA256 of "t.body">`. Signatures older than 5 minutes are rejected (`tolerance=` changes it). `webhooks.verify()` returns a bool; `webhooks.sign()` builds a header for your tests. `client.webhooks.*` uses the client's `webhook_secret`.
|
|
178
|
+
|
|
179
|
+
## Errors
|
|
180
|
+
|
|
181
|
+
Every failed call raises a `TovanoError` with `status`, `code`, `type`, `param`, `request_id` and `headers`.
|
|
182
|
+
|
|
183
|
+
| Class | Status | Code | Meaning |
|
|
184
|
+
|---|---|---|---|
|
|
185
|
+
| `AuthenticationError` | 401 | `invalid_api_key` | Missing, invalid or revoked key |
|
|
186
|
+
| `InsufficientCreditsError` | 402 | `insufficient_credits` | Add credits in the dashboard |
|
|
187
|
+
| `SpendLimitReachedError` | 402 | `spend_limit_reached` | Monthly spend limit reached |
|
|
188
|
+
| `PermissionDeniedError` | 403 | `forbidden` | |
|
|
189
|
+
| `NotFoundError` | 404 | `not_found` | Unknown model or run |
|
|
190
|
+
| `ConflictError` | 409 | `conflict` | Idempotency-Key reused with another body |
|
|
191
|
+
| `InvalidRequestError` | 422 | `invalid_request` | `param` names the field |
|
|
192
|
+
| `RateLimitError` | 429 | `rate_limited` | Retried for you; `retry_after` in seconds |
|
|
193
|
+
| `InternalServerError` | 5xx | `internal_error` | Retried for you |
|
|
194
|
+
| `ProviderError` | 502 | `provider_error` | The model provider failed; not charged |
|
|
195
|
+
| `APIConnectionError` / `APITimeoutError` | 0 | | No response |
|
|
196
|
+
| `RunTimeoutError` | | `run_timeout` | `runs.wait()` gave up; the run keeps running |
|
|
197
|
+
| `RunFailedError` | | | `runs.run(raise_on_failure=True)` |
|
|
198
|
+
|
|
199
|
+
`InsufficientCreditsError` and `SpendLimitReachedError` both subclass `PaymentRequiredError`.
|
|
200
|
+
|
|
201
|
+
**Retries:** connection errors, timeouts, 408, 429 and 5xx are retried `max_retries` times with exponential backoff and jitter. `Retry-After` is honoured up to 60 seconds. The same `Idempotency-Key` is sent on every attempt.
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
uv sync
|
|
207
|
+
uv run pytest # unit tests, plus integration tests against a local Tovano server when available
|
|
208
|
+
uv run mypy
|
|
209
|
+
uv run ruff check .
|
|
210
|
+
uv build
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tovano"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Official Python client for the Tovano API: one API key, one balance and one invoice for image, video, audio and text models."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Tovano", email = "support@tovano.ai" }]
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
dependencies = ["httpx>=0.25,<1"]
|
|
11
|
+
keywords = ["tovano", "ai", "api", "sdk", "image generation", "video generation", "text to speech", "llm", "openai compatible"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 5 - Production/Stable",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://tovano.ai"
|
|
30
|
+
Documentation = "https://tovano.ai/docs/sdk"
|
|
31
|
+
Repository = "https://github.com/tovanoai/tovano-python"
|
|
32
|
+
Issues = "https://github.com/tovanoai/tovano-python/issues"
|
|
33
|
+
Changelog = "https://github.com/tovanoai/tovano-python/blob/main/CHANGELOG.md"
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24", "ruff>=0.8", "mypy>=1.13", "pytest-timeout>=2.3"]
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["hatchling>=1.26"]
|
|
40
|
+
build-backend = "hatchling.build"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/tovano"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.sdist]
|
|
46
|
+
include = ["src/tovano", "tests", "README.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
asyncio_mode = "auto"
|
|
51
|
+
asyncio_default_fixture_loop_scope = "session"
|
|
52
|
+
asyncio_default_test_loop_scope = "session"
|
|
53
|
+
timeout = 60
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
line-length = 120
|
|
57
|
+
target-version = "py39"
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = ["E", "F", "I", "B", "UP", "W"]
|
|
61
|
+
per-file-ignores = { "examples/*" = ["E501"] }
|
|
62
|
+
ignore = ["UP006", "UP007", "UP035", "UP045", "B904", "UP037"]
|
|
63
|
+
|
|
64
|
+
[tool.mypy]
|
|
65
|
+
python_version = "3.10"
|
|
66
|
+
strict = true
|
|
67
|
+
files = ["src/tovano"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Official Python client for the Tovano API.
|
|
2
|
+
|
|
3
|
+
One API key, one prepaid balance and one invoice for image, video, audio and text models.
|
|
4
|
+
|
|
5
|
+
>>> from tovano import Tovano
|
|
6
|
+
>>> client = Tovano() # reads TOVANO_API_KEY
|
|
7
|
+
>>> run = client.runs.run(model="image/flux-1.1-pro", input={"prompt": "A blue ceramic mug"})
|
|
8
|
+
>>> run.status, run.charged_micro
|
|
9
|
+
('completed', 48000)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from . import _webhooks as webhooks
|
|
13
|
+
from ._client import AsyncTovano, Tovano
|
|
14
|
+
from ._errors import (
|
|
15
|
+
APIConnectionError,
|
|
16
|
+
APIError,
|
|
17
|
+
APITimeoutError,
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
ConflictError,
|
|
20
|
+
InsufficientCreditsError,
|
|
21
|
+
InternalServerError,
|
|
22
|
+
InvalidRequestError,
|
|
23
|
+
NotFoundError,
|
|
24
|
+
PaymentRequiredError,
|
|
25
|
+
PermissionDeniedError,
|
|
26
|
+
ProviderError,
|
|
27
|
+
RateLimitError,
|
|
28
|
+
RunFailedError,
|
|
29
|
+
RunTimeoutError,
|
|
30
|
+
SpendLimitReachedError,
|
|
31
|
+
TovanoError,
|
|
32
|
+
WebhookVerificationError,
|
|
33
|
+
)
|
|
34
|
+
from ._money import MICRO_PER_USD, format_usd, micro_to_usd, usd_to_micro
|
|
35
|
+
from ._streaming import AsyncChatCompletionStream, ChatCompletionStream, ChatStreamResult
|
|
36
|
+
from ._types import (
|
|
37
|
+
Balance,
|
|
38
|
+
ChatCompletion,
|
|
39
|
+
ChatCompletionChunk,
|
|
40
|
+
Model,
|
|
41
|
+
OutputFile,
|
|
42
|
+
Page,
|
|
43
|
+
Price,
|
|
44
|
+
Run,
|
|
45
|
+
RunError,
|
|
46
|
+
TovanoObject,
|
|
47
|
+
Usage,
|
|
48
|
+
UsageRow,
|
|
49
|
+
)
|
|
50
|
+
from ._version import __version__
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"__version__",
|
|
54
|
+
"Tovano",
|
|
55
|
+
"AsyncTovano",
|
|
56
|
+
"webhooks",
|
|
57
|
+
# errors
|
|
58
|
+
"TovanoError",
|
|
59
|
+
"APIError",
|
|
60
|
+
"InvalidRequestError",
|
|
61
|
+
"AuthenticationError",
|
|
62
|
+
"PaymentRequiredError",
|
|
63
|
+
"InsufficientCreditsError",
|
|
64
|
+
"SpendLimitReachedError",
|
|
65
|
+
"PermissionDeniedError",
|
|
66
|
+
"NotFoundError",
|
|
67
|
+
"ConflictError",
|
|
68
|
+
"RateLimitError",
|
|
69
|
+
"InternalServerError",
|
|
70
|
+
"ProviderError",
|
|
71
|
+
"APIConnectionError",
|
|
72
|
+
"APITimeoutError",
|
|
73
|
+
"RunTimeoutError",
|
|
74
|
+
"RunFailedError",
|
|
75
|
+
"WebhookVerificationError",
|
|
76
|
+
# money
|
|
77
|
+
"MICRO_PER_USD",
|
|
78
|
+
"format_usd",
|
|
79
|
+
"usd_to_micro",
|
|
80
|
+
"micro_to_usd",
|
|
81
|
+
# streaming
|
|
82
|
+
"ChatCompletionStream",
|
|
83
|
+
"AsyncChatCompletionStream",
|
|
84
|
+
"ChatStreamResult",
|
|
85
|
+
# types
|
|
86
|
+
"TovanoObject",
|
|
87
|
+
"Model",
|
|
88
|
+
"Price",
|
|
89
|
+
"Run",
|
|
90
|
+
"RunError",
|
|
91
|
+
"OutputFile",
|
|
92
|
+
"Balance",
|
|
93
|
+
"Usage",
|
|
94
|
+
"UsageRow",
|
|
95
|
+
"Page",
|
|
96
|
+
"ChatCompletion",
|
|
97
|
+
"ChatCompletionChunk",
|
|
98
|
+
]
|