runmux 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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.log
7
+ .pytest_cache/
runmux-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: runmux
3
+ Version: 0.1.0
4
+ Summary: Official RunMux Python SDK — video generation (Seedance 2.0) and the face asset library, with submit-and-wait helpers so you never hand-roll polling.
5
+ Project-URL: Homepage, https://runmux.com
6
+ Author: RunMux
7
+ License: MIT
8
+ Keywords: ai-video,runmux,sdk,seedance,video-generation
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Python: >=3.9
18
+ Requires-Dist: httpx>=0.27
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7; extra == 'dev'
21
+ Requires-Dist: respx>=0.21; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # runmux
25
+
26
+ Official Python SDK for **RunMux** — generate video with Seedance 2.0 and manage the
27
+ face asset library, without hand-rolling polling loops or the multi-step face upload.
28
+
29
+ Synchronous and Pythonic: construct one client, call resource methods with
30
+ keyword arguments, get plain dicts back.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install runmux
36
+ ```
37
+
38
+ Requires Python 3.9+ (built on [httpx](https://www.python-httpx.org/)).
39
+
40
+ ## Quickstart — text to video (one call)
41
+
42
+ ```python
43
+ import os
44
+ from runmux import RunmuxClient
45
+
46
+ client = RunmuxClient(api_key=os.environ["RUNMUX_API_KEY"])
47
+
48
+ # run() submits the job AND waits for the result — no polling code on your side.
49
+ video = client.videos.run(
50
+ model="seedance-2-0-mini",
51
+ prompt="a diamond necklace rotating on black velvet, soft highlights, product ad",
52
+ resolution="480p",
53
+ duration=5,
54
+ )
55
+
56
+ print(video["url"]) # downloadable result (expires ~1h unless you pass ttl)
57
+ ```
58
+
59
+ The API key falls back to the `RUNMUX_API_KEY` environment variable, so
60
+ `RunmuxClient()` works when that variable is set.
61
+
62
+ Prefer to manage polling yourself? Use `create()` then `wait()`:
63
+
64
+ ```python
65
+ job = client.videos.create(model="seedance-2-0-mini", prompt="...")
66
+ done = client.videos.wait(job["id"])
67
+ ```
68
+
69
+ ## Putting a person in the video (faces)
70
+
71
+ Raw human faces cannot be sent to the model directly. `faces.enroll()` runs the
72
+ whole flow — register, wait until active, return the ready-to-use `asset://`
73
+ reference:
74
+
75
+ ```python
76
+ # Use an ordinary (non-celebrity) face photo.
77
+ asset_uri = client.faces.enroll(url="https://your-cdn.com/model.jpg")
78
+
79
+ video = client.videos.run(
80
+ model="seedance-2-0-mini",
81
+ prompt="Image 1 wearing the necklace, smiling at the camera",
82
+ image_url=asset_uri, # or reference_images=[asset_uri]
83
+ )
84
+ ```
85
+
86
+ Even simpler — let RunMux enroll the face for you in one shot:
87
+
88
+ ```python
89
+ video = client.videos.run(
90
+ model="seedance-2-0-mini",
91
+ prompt="Image 1 wearing the necklace",
92
+ image_url="https://your-cdn.com/model.jpg",
93
+ auto_enroll_faces=True,
94
+ )
95
+ ```
96
+
97
+ If a face is rejected (e.g. a celebrity / copyrighted likeness), the call raises
98
+ a `RunmuxError` whose message explains why — switch to an ordinary face photo.
99
+
100
+ ## Image-to-video, batches, and inputs
101
+
102
+ ```python
103
+ # Product image as the first frame:
104
+ client.videos.run(
105
+ model="seedance-2-0-mini",
106
+ prompt="the ring rotates",
107
+ frame_images=["https://cdn/ring.jpg"],
108
+ )
109
+
110
+ # Several variations at once (number_results 1–4) returns a list:
111
+ variations = client.videos.run(
112
+ model="seedance-2-0-mini", prompt="...", number_results=3
113
+ )
114
+
115
+ # Upload a local file, then reference it:
116
+ with open("ring.png", "rb") as f:
117
+ file_url = client.files.upload(f.read(), "image/png")
118
+ ```
119
+
120
+ `frame_images` entries may be plain URL/`asset://` strings, or dicts with an
121
+ explicit frame, e.g. `{"image": "https://cdn/ring.jpg", "frame": "first"}`.
122
+
123
+ ## Webhooks
124
+
125
+ Submit with `webhook_url` to get the result POSTed to you, then verify the
126
+ signature against the raw request body:
127
+
128
+ ```python
129
+ ok = client.webhooks.verify(
130
+ payload=raw_request_body, # the raw string body, not re-serialized
131
+ signature=request.headers.get("x-runmux-signature"),
132
+ secret=os.environ["RUNMUX_WEBHOOK_SECRET"],
133
+ )
134
+ ```
135
+
136
+ The signature scheme is `sha256=<hex>` where the hex is
137
+ `HMAC-SHA256(raw_body, secret)`; comparison is constant-time.
138
+
139
+ ## Errors
140
+
141
+ Every non-2xx response (and any failed job/asset) raises a `RunmuxError` with
142
+ `.code`, `.status`, and `.request_id` for precise branching and support.
143
+
144
+ ```python
145
+ from runmux import RunmuxError
146
+
147
+ try:
148
+ client.videos.run(model="seedance-2-0-mini", prompt="...")
149
+ except RunmuxError as exc:
150
+ print(exc.code, exc.status, exc.request_id)
151
+ ```
152
+
153
+ ## API surface
154
+
155
+ - `client.videos` — `create`, `run`, `wait`, `get`
156
+ - `client.assets` — `create`, `get`, `list`, `delete`, `wait_active`
157
+ - `client.faces` — `enroll`, `enroll_asset`
158
+ - `client.files` — `create_upload`, `upload`
159
+ - `client.webhooks` — `verify`
160
+
161
+ ## Wire field names
162
+
163
+ You pass clean Python snake_case keyword arguments; the SDK maps them to the
164
+ exact field names the API expects. For example `image_url`, `reference_images`,
165
+ `auto_enroll_faces`, and `number_results` are sent as-is, while `output_type`,
166
+ `output_format`, `output_quality`, and `upload_endpoint` are sent as their
167
+ camelCase wire equivalents. Pass `idempotency_key` to dedup retries — it is sent
168
+ as the `Idempotency-Key` header, not a body field.
runmux-0.1.0/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # runmux
2
+
3
+ Official Python SDK for **RunMux** — generate video with Seedance 2.0 and manage the
4
+ face asset library, without hand-rolling polling loops or the multi-step face upload.
5
+
6
+ Synchronous and Pythonic: construct one client, call resource methods with
7
+ keyword arguments, get plain dicts back.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install runmux
13
+ ```
14
+
15
+ Requires Python 3.9+ (built on [httpx](https://www.python-httpx.org/)).
16
+
17
+ ## Quickstart — text to video (one call)
18
+
19
+ ```python
20
+ import os
21
+ from runmux import RunmuxClient
22
+
23
+ client = RunmuxClient(api_key=os.environ["RUNMUX_API_KEY"])
24
+
25
+ # run() submits the job AND waits for the result — no polling code on your side.
26
+ video = client.videos.run(
27
+ model="seedance-2-0-mini",
28
+ prompt="a diamond necklace rotating on black velvet, soft highlights, product ad",
29
+ resolution="480p",
30
+ duration=5,
31
+ )
32
+
33
+ print(video["url"]) # downloadable result (expires ~1h unless you pass ttl)
34
+ ```
35
+
36
+ The API key falls back to the `RUNMUX_API_KEY` environment variable, so
37
+ `RunmuxClient()` works when that variable is set.
38
+
39
+ Prefer to manage polling yourself? Use `create()` then `wait()`:
40
+
41
+ ```python
42
+ job = client.videos.create(model="seedance-2-0-mini", prompt="...")
43
+ done = client.videos.wait(job["id"])
44
+ ```
45
+
46
+ ## Putting a person in the video (faces)
47
+
48
+ Raw human faces cannot be sent to the model directly. `faces.enroll()` runs the
49
+ whole flow — register, wait until active, return the ready-to-use `asset://`
50
+ reference:
51
+
52
+ ```python
53
+ # Use an ordinary (non-celebrity) face photo.
54
+ asset_uri = client.faces.enroll(url="https://your-cdn.com/model.jpg")
55
+
56
+ video = client.videos.run(
57
+ model="seedance-2-0-mini",
58
+ prompt="Image 1 wearing the necklace, smiling at the camera",
59
+ image_url=asset_uri, # or reference_images=[asset_uri]
60
+ )
61
+ ```
62
+
63
+ Even simpler — let RunMux enroll the face for you in one shot:
64
+
65
+ ```python
66
+ video = client.videos.run(
67
+ model="seedance-2-0-mini",
68
+ prompt="Image 1 wearing the necklace",
69
+ image_url="https://your-cdn.com/model.jpg",
70
+ auto_enroll_faces=True,
71
+ )
72
+ ```
73
+
74
+ If a face is rejected (e.g. a celebrity / copyrighted likeness), the call raises
75
+ a `RunmuxError` whose message explains why — switch to an ordinary face photo.
76
+
77
+ ## Image-to-video, batches, and inputs
78
+
79
+ ```python
80
+ # Product image as the first frame:
81
+ client.videos.run(
82
+ model="seedance-2-0-mini",
83
+ prompt="the ring rotates",
84
+ frame_images=["https://cdn/ring.jpg"],
85
+ )
86
+
87
+ # Several variations at once (number_results 1–4) returns a list:
88
+ variations = client.videos.run(
89
+ model="seedance-2-0-mini", prompt="...", number_results=3
90
+ )
91
+
92
+ # Upload a local file, then reference it:
93
+ with open("ring.png", "rb") as f:
94
+ file_url = client.files.upload(f.read(), "image/png")
95
+ ```
96
+
97
+ `frame_images` entries may be plain URL/`asset://` strings, or dicts with an
98
+ explicit frame, e.g. `{"image": "https://cdn/ring.jpg", "frame": "first"}`.
99
+
100
+ ## Webhooks
101
+
102
+ Submit with `webhook_url` to get the result POSTed to you, then verify the
103
+ signature against the raw request body:
104
+
105
+ ```python
106
+ ok = client.webhooks.verify(
107
+ payload=raw_request_body, # the raw string body, not re-serialized
108
+ signature=request.headers.get("x-runmux-signature"),
109
+ secret=os.environ["RUNMUX_WEBHOOK_SECRET"],
110
+ )
111
+ ```
112
+
113
+ The signature scheme is `sha256=<hex>` where the hex is
114
+ `HMAC-SHA256(raw_body, secret)`; comparison is constant-time.
115
+
116
+ ## Errors
117
+
118
+ Every non-2xx response (and any failed job/asset) raises a `RunmuxError` with
119
+ `.code`, `.status`, and `.request_id` for precise branching and support.
120
+
121
+ ```python
122
+ from runmux import RunmuxError
123
+
124
+ try:
125
+ client.videos.run(model="seedance-2-0-mini", prompt="...")
126
+ except RunmuxError as exc:
127
+ print(exc.code, exc.status, exc.request_id)
128
+ ```
129
+
130
+ ## API surface
131
+
132
+ - `client.videos` — `create`, `run`, `wait`, `get`
133
+ - `client.assets` — `create`, `get`, `list`, `delete`, `wait_active`
134
+ - `client.faces` — `enroll`, `enroll_asset`
135
+ - `client.files` — `create_upload`, `upload`
136
+ - `client.webhooks` — `verify`
137
+
138
+ ## Wire field names
139
+
140
+ You pass clean Python snake_case keyword arguments; the SDK maps them to the
141
+ exact field names the API expects. For example `image_url`, `reference_images`,
142
+ `auto_enroll_faces`, and `number_results` are sent as-is, while `output_type`,
143
+ `output_format`, `output_quality`, and `upload_endpoint` are sent as their
144
+ camelCase wire equivalents. Pass `idempotency_key` to dedup retries — it is sent
145
+ as the `Idempotency-Key` header, not a body field.
@@ -0,0 +1,22 @@
1
+ """Put a specific person (ordinary, non-celebrity face) into a jewelry shot.
2
+
3
+ Run: RUNMUX_API_KEY=sk-... python examples/face_portrait.py
4
+ """
5
+
6
+ import os
7
+
8
+ from runmux import RunmuxClient
9
+
10
+ client = RunmuxClient(api_key=os.environ.get("RUNMUX_API_KEY"))
11
+
12
+ # One-call onboarding: register the face and get its asset:// reference.
13
+ model = client.faces.enroll(url="https://your-cdn.com/model.jpg", name="model")
14
+
15
+ video = client.videos.run(
16
+ model="seedance-2-0-mini",
17
+ prompt="Image 1 wearing a diamond ring, raising her hand to show it, product ad",
18
+ reference_images=[model, "https://your-cdn.com/ring.jpg"],
19
+ resolution="480p",
20
+ )
21
+
22
+ print("Portrait video:", video["url"])
@@ -0,0 +1,19 @@
1
+ """Quickstart: text to video in one call.
2
+
3
+ Run: RUNMUX_API_KEY=sk-... python examples/quickstart.py
4
+ """
5
+
6
+ import os
7
+
8
+ from runmux import RunmuxClient
9
+
10
+ client = RunmuxClient(api_key=os.environ.get("RUNMUX_API_KEY"))
11
+
12
+ video = client.videos.run(
13
+ model="seedance-2-0-mini",
14
+ prompt="a hot air balloon rising over green hills at dawn, cinematic",
15
+ resolution="480p",
16
+ duration=5,
17
+ )
18
+
19
+ print("Done:", video["url"])
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "runmux"
7
+ version = "0.1.0"
8
+ description = "Official RunMux Python SDK — video generation (Seedance 2.0) and the face asset library, with submit-and-wait helpers so you never hand-roll polling."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "RunMux" }]
13
+ keywords = ["runmux", "video-generation", "seedance", "ai-video", "sdk"]
14
+ classifiers = [
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Intended Audience :: Developers",
23
+ ]
24
+ dependencies = ["httpx>=0.27"]
25
+
26
+ [project.optional-dependencies]
27
+ dev = ["pytest>=7", "respx>=0.21"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://runmux.com"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["runmux"]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
@@ -0,0 +1,28 @@
1
+ """RunMux — official Python SDK.
2
+
3
+ Generate video with Seedance 2.0 and manage the face asset library, with
4
+ submit-and-wait helpers so you never hand-roll polling.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .assets import Assets
10
+ from .client import RunmuxClient
11
+ from .errors import RunmuxError
12
+ from .faces import Faces
13
+ from .files import Files
14
+ from .videos import Videos
15
+ from .webhooks import Webhooks
16
+
17
+ __version__ = "0.1.0"
18
+
19
+ __all__ = [
20
+ "RunmuxClient",
21
+ "RunmuxError",
22
+ "Videos",
23
+ "Assets",
24
+ "Faces",
25
+ "Files",
26
+ "Webhooks",
27
+ "__version__",
28
+ ]
@@ -0,0 +1,83 @@
1
+ """The face / portrait asset library: register media and poll until active."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
7
+ from urllib.parse import quote
8
+
9
+ from .errors import RunmuxError
10
+
11
+ if TYPE_CHECKING: # pragma: no cover
12
+ from .client import RunmuxClient
13
+
14
+
15
+ class Assets:
16
+ def __init__(self, client: "RunmuxClient") -> None:
17
+ self._client = client
18
+
19
+ def create(
20
+ self,
21
+ *,
22
+ url: Optional[str] = None,
23
+ data_url: Optional[str] = None,
24
+ name: Optional[str] = None,
25
+ asset_type: Optional[str] = None,
26
+ ) -> Dict[str, Any]:
27
+ """Register a face/portrait asset by public URL or inline ``data_url``.
28
+
29
+ Returns a ``processing`` record. ``asset_type`` is ``"Image"`` or
30
+ ``"Video"``.
31
+ """
32
+ body: Dict[str, Any] = {}
33
+ if url is not None:
34
+ body["url"] = url
35
+ if data_url is not None:
36
+ body["dataUrl"] = data_url
37
+ if name is not None:
38
+ body["name"] = name
39
+ if asset_type is not None:
40
+ body["assetType"] = asset_type
41
+ return self._client.request("POST", "/v1/assets", body=body)
42
+
43
+ def get(self, id: str) -> Dict[str, Any]:
44
+ return self._client.request("GET", f"/v1/assets/{quote(id, safe='')}")
45
+
46
+ def list(self) -> List[Dict[str, Any]]:
47
+ res = self._client.request("GET", "/v1/assets")
48
+ if isinstance(res, dict):
49
+ return res.get("data") or []
50
+ return []
51
+
52
+ def delete(self, id: str) -> None:
53
+ self._client.request("DELETE", f"/v1/assets/{quote(id, safe='')}")
54
+
55
+ def wait_active(
56
+ self,
57
+ id: str,
58
+ interval: float = 3,
59
+ timeout: float = 120,
60
+ ) -> Dict[str, Any]:
61
+ """Poll an asset until it leaves ``processing``.
62
+
63
+ Returns the active asset record. Raises ``RunmuxError`` (carrying
64
+ ``statusReason``) if the asset fails or is rejected, or on timeout
65
+ (seconds).
66
+ """
67
+ deadline = time.monotonic() + timeout
68
+ while True:
69
+ asset = self.get(id)
70
+ status = asset.get("status")
71
+ if status == "active":
72
+ return asset
73
+ if status in ("failed", "rejected"):
74
+ raise RunmuxError(
75
+ asset.get("statusReason") or "Asset failed processing.",
76
+ code="bad_request",
77
+ )
78
+ if time.monotonic() >= deadline:
79
+ raise RunmuxError(
80
+ f"Timed out waiting for asset {id} to become active.",
81
+ code="upstream_timeout",
82
+ )
83
+ time.sleep(interval)
@@ -0,0 +1,132 @@
1
+ """The RunMux client — construct once with your API key, then use the resource
2
+ namespaces: ``client.videos``, ``client.files``, ``client.assets``,
3
+ ``client.faces``, ``client.webhooks``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from typing import Any, Dict, Mapping, Optional
10
+
11
+ import httpx
12
+
13
+ from .assets import Assets
14
+ from .errors import RunmuxError
15
+ from .faces import Faces
16
+ from .files import Files
17
+ from .videos import Videos
18
+ from .webhooks import Webhooks
19
+
20
+ DEFAULT_BASE_URL = "https://api.runmux.com"
21
+
22
+
23
+ class RunmuxClient:
24
+ """Synchronous RunMux client.
25
+
26
+ Args:
27
+ api_key: Your RunMux API key (Bearer). Falls back to the
28
+ ``RUNMUX_API_KEY`` environment variable.
29
+ base_url: API base URL. Default ``https://api.runmux.com``.
30
+ timeout: Per-request timeout in seconds. Default ``60``.
31
+ transport: Optional ``httpx`` transport override (for tests / custom
32
+ transports).
33
+
34
+ Raises:
35
+ RunmuxError: with ``code="invalid_key"`` if no API key is provided.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ api_key: Optional[str] = None,
41
+ base_url: str = DEFAULT_BASE_URL,
42
+ timeout: float = 60,
43
+ *,
44
+ transport: Optional[httpx.BaseTransport] = None,
45
+ ) -> None:
46
+ key = api_key if api_key is not None else os.environ.get("RUNMUX_API_KEY")
47
+ if not key:
48
+ raise RunmuxError(
49
+ "A RunMux API key is required (pass api_key or set RUNMUX_API_KEY).",
50
+ code="invalid_key",
51
+ )
52
+ self._api_key = key
53
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
54
+ self.timeout = timeout
55
+ self._http = httpx.Client(
56
+ base_url=self.base_url,
57
+ timeout=timeout,
58
+ transport=transport,
59
+ headers={
60
+ "authorization": f"Bearer {self._api_key}",
61
+ "content-type": "application/json",
62
+ },
63
+ )
64
+
65
+ self.videos = Videos(self)
66
+ self.files = Files(self)
67
+ self.assets = Assets(self)
68
+ self.faces = Faces(self)
69
+ self.webhooks = Webhooks()
70
+
71
+ # -- context manager / lifecycle ------------------------------------
72
+
73
+ def close(self) -> None:
74
+ """Close the underlying HTTP connection pool."""
75
+ self._http.close()
76
+
77
+ def __enter__(self) -> "RunmuxClient":
78
+ return self
79
+
80
+ def __exit__(self, *exc: Any) -> None:
81
+ self.close()
82
+
83
+ # -- low-level request ----------------------------------------------
84
+
85
+ def request(
86
+ self,
87
+ method: str,
88
+ path: str,
89
+ *,
90
+ body: Any = None,
91
+ headers: Optional[Mapping[str, str]] = None,
92
+ ) -> Any:
93
+ """Low-level JSON request against the RunMux API.
94
+
95
+ Raises ``RunmuxError`` on any non-2xx response, parsing the API's
96
+ ``{"error": {code, message, request_id}}`` envelope when present.
97
+ """
98
+ try:
99
+ response = self._http.request(
100
+ method,
101
+ path,
102
+ json=body if body is not None else None,
103
+ headers=dict(headers) if headers else None,
104
+ )
105
+ except httpx.TimeoutException as exc:
106
+ raise RunmuxError(
107
+ "Request to RunMux timed out.", code="upstream_timeout"
108
+ ) from exc
109
+ except httpx.HTTPError as exc:
110
+ raise RunmuxError(
111
+ str(exc) or "Network request failed.", code="upstream_unavailable"
112
+ ) from exc
113
+
114
+ text = response.text
115
+ data: Any = None
116
+ if text:
117
+ try:
118
+ data = response.json()
119
+ except ValueError:
120
+ data = None
121
+
122
+ if not response.is_success:
123
+ err: Dict[str, Any] = {}
124
+ if isinstance(data, dict) and isinstance(data.get("error"), dict):
125
+ err = data["error"]
126
+ raise RunmuxError(
127
+ err.get("message") or f"RunMux request failed ({response.status_code}).",
128
+ status=response.status_code,
129
+ code=err.get("code") or "upstream_unavailable",
130
+ request_id=err.get("request_id"),
131
+ )
132
+ return data
@@ -0,0 +1,54 @@
1
+ """Error type for the RunMux SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ # Stable error codes returned by the RunMux API (mirrors the server's error codes).
8
+ ERROR_CODES = (
9
+ "invalid_key",
10
+ "insufficient_balance",
11
+ "model_not_allowed",
12
+ "rate_limited",
13
+ "upstream_timeout",
14
+ "stream_interrupted",
15
+ "upstream_unavailable",
16
+ "internal_error",
17
+ "permission_denied",
18
+ "bad_request",
19
+ )
20
+
21
+
22
+ class RunmuxError(Exception):
23
+ """Raised for any non-2xx RunMux API response, or when a job/asset reaches a
24
+ failed terminal state.
25
+
26
+ Carries the API's ``code`` and ``request_id`` so callers can branch and
27
+ report precisely.
28
+
29
+ Attributes:
30
+ message: Human-readable description.
31
+ status: HTTP status code (0 when not from an HTTP response).
32
+ code: A stable machine-readable error code (see ``ERROR_CODES``).
33
+ request_id: Server-side request id for support, when available.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ message: str,
39
+ *,
40
+ status: int = 0,
41
+ code: str = "internal_error",
42
+ request_id: Optional[str] = None,
43
+ ) -> None:
44
+ super().__init__(message)
45
+ self.message = message
46
+ self.status = status
47
+ self.code = code
48
+ self.request_id = request_id
49
+
50
+ def __repr__(self) -> str: # pragma: no cover - cosmetic
51
+ return (
52
+ f"RunmuxError(message={self.message!r}, status={self.status!r}, "
53
+ f"code={self.code!r}, request_id={self.request_id!r})"
54
+ )