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.
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ .DS_Store
9
+ .env
@@ -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
+ ]