tunova 0.1.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.
tunova-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tunova
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.
tunova-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: tunova
3
+ Version: 0.1.0
4
+ Summary: Tiny zero-dependency Python client for the Tunova Suno music API (v5.5) — async, billed only on success.
5
+ Author: Tunova
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Tunova
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://tunova.ai
29
+ Project-URL: Documentation, https://api.tunova.ai/docs
30
+ Project-URL: Source, https://github.com/erliona/tunova-sdk
31
+ Keywords: suno,suno-api,music,ai,mcp,api-client
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Topic :: Multimedia :: Sound/Audio
38
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
39
+ Requires-Python: >=3.8
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Dynamic: license-file
43
+
44
+ # Tunova SDK
45
+
46
+ Tiny, **zero-dependency** clients + MCP manifest for the [Tunova](https://tunova.ai) music API —
47
+ generate music with Suno (v5.5) over a simple REST or MCP interface.
48
+ Generation is async and **billed only on success**: a failed render refunds itself.
49
+
50
+ - **Python** → [`python/tunova.py`](python/tunova.py) — stdlib only, Python 3.8+.
51
+ - **Node / TypeScript** → [`node/tunova.ts`](node/tunova.ts) — Node 18+ (`fetch` built in).
52
+ - **MCP** → [`server.json`](server.json) — hosted Streamable-HTTP server at `https://api.tunova.ai/mcp`.
53
+
54
+ Get a key (50 free tokens, no card) at **<https://tunova.ai>**. Full reference:
55
+ <https://api.tunova.ai/docs> · live status: <https://tunova.ai/status>.
56
+
57
+ > Tunova is an independent service, not affiliated with or endorsed by Suno. Tracks come from paid
58
+ > Suno plans; review Suno's terms for your use case.
59
+
60
+ ## Python
61
+
62
+ ```bash
63
+ pip install tunova
64
+ ```
65
+
66
+ ```python
67
+ from tunova import Tunova
68
+
69
+ t = Tunova("sk_live_…")
70
+ job = t.generate("warm lo-fi piano to study to", model="v5.5")
71
+ print(job["clips"][0]["audio_url"] if job["status"] == "complete" else job["error"])
72
+ ```
73
+
74
+ Prefer no install? `python/tunova.py` is stdlib-only — vendor the single file and `import` it.
75
+
76
+ ## Node / TypeScript
77
+
78
+ ```bash
79
+ npm i tunova
80
+ ```
81
+
82
+ ```ts
83
+ import { Tunova } from "tunova";
84
+
85
+ const t = new Tunova(process.env.TUNOVA_API_KEY!);
86
+ const job = await t.generate("warm lo-fi piano to study to", { model: "v5.5" });
87
+ console.log(job.status === "complete" ? job.clips[0]?.audio_url : job.error);
88
+ ```
89
+
90
+ Both `generate()` calls submit a job and poll until the track is delivered. Prefer fire-and-forget?
91
+ Use `submit()` with a `callback_url` and take an HMAC-signed webhook instead.
92
+
93
+ ## MCP — give an AI agent the power to make music
94
+
95
+ ```bash
96
+ claude mcp add --transport http tunova https://api.tunova.ai/mcp \
97
+ --header "X-API-Key: sk_live_…"
98
+ ```
99
+
100
+ Tools: `generate_song` · `wait_for_song` · `check_song`. Same API key, same billed-on-success rule.
101
+
102
+ ## Verifying webhooks
103
+
104
+ If you pass `callback_url`, Tunova POSTs the terminal job with
105
+ `X-Webhook-Signature: sha256=<hmac>` over `<X-Webhook-Timestamp>.<rawBody>`, keyed with your
106
+ `whsec_…` secret (view/rotate it on the dashboard's API-keys page). Verify against the **raw** body
107
+ before parsing — both SDKs ship a verifier (`Tunova.verify_webhook` / `verifyWebhook`).
108
+
109
+ ## License
110
+
111
+ MIT — see [LICENSE](LICENSE). The SDK code is yours to use freely; your use of the Tunova API is
112
+ governed by the [terms](https://tunova.ai/terms).
tunova-0.1.0/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # Tunova SDK
2
+
3
+ Tiny, **zero-dependency** clients + MCP manifest for the [Tunova](https://tunova.ai) music API —
4
+ generate music with Suno (v5.5) over a simple REST or MCP interface.
5
+ Generation is async and **billed only on success**: a failed render refunds itself.
6
+
7
+ - **Python** → [`python/tunova.py`](python/tunova.py) — stdlib only, Python 3.8+.
8
+ - **Node / TypeScript** → [`node/tunova.ts`](node/tunova.ts) — Node 18+ (`fetch` built in).
9
+ - **MCP** → [`server.json`](server.json) — hosted Streamable-HTTP server at `https://api.tunova.ai/mcp`.
10
+
11
+ Get a key (50 free tokens, no card) at **<https://tunova.ai>**. Full reference:
12
+ <https://api.tunova.ai/docs> · live status: <https://tunova.ai/status>.
13
+
14
+ > Tunova is an independent service, not affiliated with or endorsed by Suno. Tracks come from paid
15
+ > Suno plans; review Suno's terms for your use case.
16
+
17
+ ## Python
18
+
19
+ ```bash
20
+ pip install tunova
21
+ ```
22
+
23
+ ```python
24
+ from tunova import Tunova
25
+
26
+ t = Tunova("sk_live_…")
27
+ job = t.generate("warm lo-fi piano to study to", model="v5.5")
28
+ print(job["clips"][0]["audio_url"] if job["status"] == "complete" else job["error"])
29
+ ```
30
+
31
+ Prefer no install? `python/tunova.py` is stdlib-only — vendor the single file and `import` it.
32
+
33
+ ## Node / TypeScript
34
+
35
+ ```bash
36
+ npm i tunova
37
+ ```
38
+
39
+ ```ts
40
+ import { Tunova } from "tunova";
41
+
42
+ const t = new Tunova(process.env.TUNOVA_API_KEY!);
43
+ const job = await t.generate("warm lo-fi piano to study to", { model: "v5.5" });
44
+ console.log(job.status === "complete" ? job.clips[0]?.audio_url : job.error);
45
+ ```
46
+
47
+ Both `generate()` calls submit a job and poll until the track is delivered. Prefer fire-and-forget?
48
+ Use `submit()` with a `callback_url` and take an HMAC-signed webhook instead.
49
+
50
+ ## MCP — give an AI agent the power to make music
51
+
52
+ ```bash
53
+ claude mcp add --transport http tunova https://api.tunova.ai/mcp \
54
+ --header "X-API-Key: sk_live_…"
55
+ ```
56
+
57
+ Tools: `generate_song` · `wait_for_song` · `check_song`. Same API key, same billed-on-success rule.
58
+
59
+ ## Verifying webhooks
60
+
61
+ If you pass `callback_url`, Tunova POSTs the terminal job with
62
+ `X-Webhook-Signature: sha256=<hmac>` over `<X-Webhook-Timestamp>.<rawBody>`, keyed with your
63
+ `whsec_…` secret (view/rotate it on the dashboard's API-keys page). Verify against the **raw** body
64
+ before parsing — both SDKs ship a verifier (`Tunova.verify_webhook` / `verifyWebhook`).
65
+
66
+ ## License
67
+
68
+ MIT — see [LICENSE](LICENSE). The SDK code is yours to use freely; your use of the Tunova API is
69
+ governed by the [terms](https://tunova.ai/terms).
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tunova"
7
+ version = "0.1.0"
8
+ description = "Tiny zero-dependency Python client for the Tunova Suno music API (v5.5) — async, billed only on success."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Tunova" }]
13
+ keywords = ["suno", "suno-api", "music", "ai", "mcp", "api-client"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Multimedia :: Sound/Audio",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://tunova.ai"
26
+ Documentation = "https://api.tunova.ai/docs"
27
+ Source = "https://github.com/erliona/tunova-sdk"
28
+
29
+ # The client is a single stdlib-only module living in python/tunova.py.
30
+ [tool.setuptools]
31
+ py-modules = ["tunova"]
32
+
33
+ [tool.setuptools.package-dir]
34
+ "" = "python"
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: tunova
3
+ Version: 0.1.0
4
+ Summary: Tiny zero-dependency Python client for the Tunova Suno music API (v5.5) — async, billed only on success.
5
+ Author: Tunova
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Tunova
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://tunova.ai
29
+ Project-URL: Documentation, https://api.tunova.ai/docs
30
+ Project-URL: Source, https://github.com/erliona/tunova-sdk
31
+ Keywords: suno,suno-api,music,ai,mcp,api-client
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Topic :: Multimedia :: Sound/Audio
38
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
39
+ Requires-Python: >=3.8
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Dynamic: license-file
43
+
44
+ # Tunova SDK
45
+
46
+ Tiny, **zero-dependency** clients + MCP manifest for the [Tunova](https://tunova.ai) music API —
47
+ generate music with Suno (v5.5) over a simple REST or MCP interface.
48
+ Generation is async and **billed only on success**: a failed render refunds itself.
49
+
50
+ - **Python** → [`python/tunova.py`](python/tunova.py) — stdlib only, Python 3.8+.
51
+ - **Node / TypeScript** → [`node/tunova.ts`](node/tunova.ts) — Node 18+ (`fetch` built in).
52
+ - **MCP** → [`server.json`](server.json) — hosted Streamable-HTTP server at `https://api.tunova.ai/mcp`.
53
+
54
+ Get a key (50 free tokens, no card) at **<https://tunova.ai>**. Full reference:
55
+ <https://api.tunova.ai/docs> · live status: <https://tunova.ai/status>.
56
+
57
+ > Tunova is an independent service, not affiliated with or endorsed by Suno. Tracks come from paid
58
+ > Suno plans; review Suno's terms for your use case.
59
+
60
+ ## Python
61
+
62
+ ```bash
63
+ pip install tunova
64
+ ```
65
+
66
+ ```python
67
+ from tunova import Tunova
68
+
69
+ t = Tunova("sk_live_…")
70
+ job = t.generate("warm lo-fi piano to study to", model="v5.5")
71
+ print(job["clips"][0]["audio_url"] if job["status"] == "complete" else job["error"])
72
+ ```
73
+
74
+ Prefer no install? `python/tunova.py` is stdlib-only — vendor the single file and `import` it.
75
+
76
+ ## Node / TypeScript
77
+
78
+ ```bash
79
+ npm i tunova
80
+ ```
81
+
82
+ ```ts
83
+ import { Tunova } from "tunova";
84
+
85
+ const t = new Tunova(process.env.TUNOVA_API_KEY!);
86
+ const job = await t.generate("warm lo-fi piano to study to", { model: "v5.5" });
87
+ console.log(job.status === "complete" ? job.clips[0]?.audio_url : job.error);
88
+ ```
89
+
90
+ Both `generate()` calls submit a job and poll until the track is delivered. Prefer fire-and-forget?
91
+ Use `submit()` with a `callback_url` and take an HMAC-signed webhook instead.
92
+
93
+ ## MCP — give an AI agent the power to make music
94
+
95
+ ```bash
96
+ claude mcp add --transport http tunova https://api.tunova.ai/mcp \
97
+ --header "X-API-Key: sk_live_…"
98
+ ```
99
+
100
+ Tools: `generate_song` · `wait_for_song` · `check_song`. Same API key, same billed-on-success rule.
101
+
102
+ ## Verifying webhooks
103
+
104
+ If you pass `callback_url`, Tunova POSTs the terminal job with
105
+ `X-Webhook-Signature: sha256=<hmac>` over `<X-Webhook-Timestamp>.<rawBody>`, keyed with your
106
+ `whsec_…` secret (view/rotate it on the dashboard's API-keys page). Verify against the **raw** body
107
+ before parsing — both SDKs ship a verifier (`Tunova.verify_webhook` / `verifyWebhook`).
108
+
109
+ ## License
110
+
111
+ MIT — see [LICENSE](LICENSE). The SDK code is yours to use freely; your use of the Tunova API is
112
+ governed by the [terms](https://tunova.ai/terms).
@@ -0,0 +1,8 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ python/tunova.py
5
+ python/tunova.egg-info/PKG-INFO
6
+ python/tunova.egg-info/SOURCES.txt
7
+ python/tunova.egg-info/dependency_links.txt
8
+ python/tunova.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ tunova
@@ -0,0 +1,159 @@
1
+ """Tunova — tiny zero-dependency Python client for the Suno music API (https://api.tunova.ai).
2
+
3
+ Generation is asynchronous: ``submit()`` returns a ``job_id``; ``generate()`` submits and
4
+ polls until the track is delivered. You're billed only when a song actually delivers —
5
+ failed renders are auto-refunded.
6
+
7
+ from tunova import Tunova
8
+
9
+ t = Tunova("sk_live_…")
10
+ job = t.generate("lofi hip hop to code to", model="v5.5")
11
+ if job["status"] == "complete":
12
+ print(job["clips"][0]["audio_url"])
13
+ else:
14
+ print("failed (auto-refunded):", job["error"])
15
+
16
+ Stdlib only — no pip install needed. Python 3.8+.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import hmac
22
+ import json
23
+ import time
24
+ import urllib.error
25
+ import urllib.request
26
+ from typing import Any, Dict, Optional
27
+
28
+ DEFAULT_BASE = "https://api.tunova.ai"
29
+ _TERMINAL = ("complete", "failed")
30
+
31
+
32
+ class TunovaError(Exception):
33
+ """An API error. ``code``/``detail`` mirror the JSON error envelope; ``request_id`` is the
34
+ X-Request-Id you can quote to support."""
35
+
36
+ def __init__(self, status: int, code: str, detail: str, request_id: Optional[str] = None):
37
+ super().__init__(f"[{status}] {code}: {detail}")
38
+ self.status = status
39
+ self.code = code
40
+ self.detail = detail
41
+ self.request_id = request_id
42
+
43
+
44
+ class Tunova:
45
+ def __init__(self, api_key: str, base_url: str = DEFAULT_BASE, timeout: float = 30.0):
46
+ if not api_key:
47
+ raise ValueError("api_key is required (your sk_live_… key)")
48
+ self.api_key = api_key
49
+ self.base_url = base_url.rstrip("/")
50
+ self.timeout = timeout
51
+
52
+ # ---- transport ----
53
+ def _request(self, method: str, path: str, headers: Dict[str, str], data: Optional[bytes]) -> Any:
54
+ req = urllib.request.Request(self.base_url + path, data=data, headers=headers, method=method)
55
+ try:
56
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
57
+ raw = resp.read()
58
+ return json.loads(raw) if raw else {}
59
+ except urllib.error.HTTPError as e:
60
+ raw = e.read()
61
+ try:
62
+ body = json.loads(raw)
63
+ except Exception:
64
+ body = {}
65
+ raise TunovaError(
66
+ e.code,
67
+ body.get("code", "HTTP_ERROR"),
68
+ body.get("detail", raw.decode("utf-8", "replace")[:300]),
69
+ body.get("request_id"),
70
+ ) from None
71
+
72
+ def _post(self, path: str, body: Dict[str, Any], idempotency_key: Optional[str] = None) -> Any:
73
+ headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
74
+ if idempotency_key:
75
+ headers["Idempotency-Key"] = idempotency_key
76
+ return self._request("POST", path, headers, json.dumps(body).encode())
77
+
78
+ def _get(self, path: str) -> Any:
79
+ return self._request("GET", path, {"X-API-Key": self.api_key}, None)
80
+
81
+ # ---- API ----
82
+ def submit(
83
+ self,
84
+ prompt: str,
85
+ *,
86
+ custom: bool = False,
87
+ tags: Optional[str] = None,
88
+ title: Optional[str] = None,
89
+ make_instrumental: bool = False,
90
+ model: Optional[str] = None,
91
+ callback_url: Optional[str] = None,
92
+ idempotency_key: Optional[str] = None,
93
+ ) -> Dict[str, Any]:
94
+ """Submit a generation job (returns immediately). Response: ``{job_id, status, status_url}``.
95
+
96
+ ``custom=True`` switches to lyrics mode (``prompt`` = your lyrics; add ``tags``/``title``).
97
+ ``model`` is e.g. "v5.5". Pass ``callback_url`` for an HMAC-signed webhook on
98
+ completion, or poll ``get_job``. ``idempotency_key`` makes a retried submit return the same
99
+ job (never double-charged)."""
100
+ body: Dict[str, Any] = {"prompt": prompt}
101
+ if custom:
102
+ if tags is not None:
103
+ body["tags"] = tags
104
+ if title is not None:
105
+ body["title"] = title
106
+ if make_instrumental:
107
+ body["make_instrumental"] = True
108
+ if model:
109
+ body["model"] = model
110
+ if callback_url:
111
+ body["callback_url"] = callback_url
112
+ path = "/api/custom_generate" if custom else "/api/generate"
113
+ return self._post(path, body, idempotency_key)
114
+
115
+ def get_job(self, job_id: str) -> Dict[str, Any]:
116
+ """Fetch a job's current state. Once ``status == "complete"``, ``clips[].audio_url`` is set."""
117
+ return self._get(f"/api/jobs/{job_id}")
118
+
119
+ def wait_for(self, job_id: str, *, poll_interval: float = 3.0, timeout: float = 300.0) -> Dict[str, Any]:
120
+ """Poll until the job is terminal ("complete"/"failed") or ``timeout`` seconds elapse."""
121
+ deadline = time.monotonic() + timeout
122
+ while True:
123
+ job = self.get_job(job_id)
124
+ if job.get("status") in _TERMINAL:
125
+ return job
126
+ if time.monotonic() >= deadline:
127
+ raise TunovaError(0, "TIMEOUT", f"job {job_id} not terminal after {timeout}s")
128
+ time.sleep(poll_interval)
129
+
130
+ def generate(self, prompt: str, *, poll_interval: float = 3.0, timeout: float = 300.0, **kwargs: Any) -> Dict[str, Any]:
131
+ """``submit`` + ``wait_for``. Returns the terminal job — check ``job["status"]``
132
+ ("complete" or "failed"). All ``submit`` keyword args are forwarded."""
133
+ accepted = self.submit(prompt, **kwargs)
134
+ return self.wait_for(accepted["job_id"], poll_interval=poll_interval, timeout=timeout)
135
+
136
+ # ---- webhooks ----
137
+ @staticmethod
138
+ def verify_webhook(
139
+ secret: str,
140
+ timestamp: str,
141
+ body: str,
142
+ signature: str,
143
+ *,
144
+ tolerance: int = 300,
145
+ ) -> bool:
146
+ """Verify a webhook delivery. ``signature`` = the ``X-Webhook-Signature`` header
147
+ ("sha256=<hex>"), ``timestamp`` = ``X-Webhook-Timestamp``, ``body`` = the RAW request body
148
+ string (verify before parsing), ``secret`` = your ``whsec_…`` key. Returns True iff the
149
+ signature matches AND the timestamp is within ``tolerance`` seconds (anti-replay; pass
150
+ ``tolerance=0`` to skip the freshness check)."""
151
+ if tolerance:
152
+ try:
153
+ if abs(time.time() - int(timestamp)) > tolerance:
154
+ return False
155
+ except (TypeError, ValueError):
156
+ return False
157
+ expected = hmac.new(secret.encode(), f"{timestamp}.{body}".encode(), hashlib.sha256).hexdigest()
158
+ provided = signature[7:] if signature.startswith("sha256=") else signature
159
+ return hmac.compare_digest(expected, provided)
tunova-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+