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.
- runmux-0.1.0/.gitignore +7 -0
- runmux-0.1.0/PKG-INFO +168 -0
- runmux-0.1.0/README.md +145 -0
- runmux-0.1.0/examples/face_portrait.py +22 -0
- runmux-0.1.0/examples/quickstart.py +19 -0
- runmux-0.1.0/pyproject.toml +36 -0
- runmux-0.1.0/runmux/__init__.py +28 -0
- runmux-0.1.0/runmux/assets.py +83 -0
- runmux-0.1.0/runmux/client.py +132 -0
- runmux-0.1.0/runmux/errors.py +54 -0
- runmux-0.1.0/runmux/faces.py +59 -0
- runmux-0.1.0/runmux/files.py +64 -0
- runmux-0.1.0/runmux/videos.py +163 -0
- runmux-0.1.0/runmux/webhooks.py +32 -0
- runmux-0.1.0/tests/__init__.py +0 -0
- runmux-0.1.0/tests/conftest.py +44 -0
- runmux-0.1.0/tests/test_assets_faces.py +126 -0
- runmux-0.1.0/tests/test_client.py +54 -0
- runmux-0.1.0/tests/test_files.py +81 -0
- runmux-0.1.0/tests/test_videos.py +140 -0
- runmux-0.1.0/tests/test_webhooks.py +36 -0
runmux-0.1.0/.gitignore
ADDED
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
|
+
)
|