martini-client 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.
- martini_client-0.1.0/.gitignore +8 -0
- martini_client-0.1.0/AGENTS.md +40 -0
- martini_client-0.1.0/LICENSE +21 -0
- martini_client-0.1.0/PKG-INFO +289 -0
- martini_client-0.1.0/README.md +265 -0
- martini_client-0.1.0/pyproject.toml +81 -0
- martini_client-0.1.0/scripts/gen_overloads.py +112 -0
- martini_client-0.1.0/src/martini_client/__init__.py +104 -0
- martini_client-0.1.0/src/martini_client/_client.py +191 -0
- martini_client-0.1.0/src/martini_client/_families.py +95 -0
- martini_client-0.1.0/src/martini_client/_http.py +280 -0
- martini_client-0.1.0/src/martini_client/_polling.py +101 -0
- martini_client-0.1.0/src/martini_client/_version.py +17 -0
- martini_client-0.1.0/src/martini_client/_wire.py +142 -0
- martini_client-0.1.0/src/martini_client/endpoints.py +104 -0
- martini_client-0.1.0/src/martini_client/errors.py +236 -0
- martini_client-0.1.0/src/martini_client/py.typed +0 -0
- martini_client-0.1.0/src/martini_client/resources/__init__.py +1 -0
- martini_client-0.1.0/src/martini_client/resources/assets.py +548 -0
- martini_client-0.1.0/src/martini_client/resources/generations.py +1825 -0
- martini_client-0.1.0/src/martini_client/resources/workflows.py +362 -0
- martini_client-0.1.0/src/martini_client/types/__init__.py +151 -0
- martini_client-0.1.0/src/martini_client/types/assets.py +200 -0
- martini_client-0.1.0/src/martini_client/types/common.py +45 -0
- martini_client-0.1.0/src/martini_client/types/generations.py +153 -0
- martini_client-0.1.0/src/martini_client/types/inputs.py +443 -0
- martini_client-0.1.0/src/martini_client/types/models.py +171 -0
- martini_client-0.1.0/src/martini_client/types/workflows.py +537 -0
- martini_client-0.1.0/tests/__init__.py +0 -0
- martini_client-0.1.0/tests/conftest.py +204 -0
- martini_client-0.1.0/tests/test_assets.py +319 -0
- martini_client-0.1.0/tests/test_client.py +151 -0
- martini_client-0.1.0/tests/test_errors_and_retries.py +142 -0
- martini_client-0.1.0/tests/test_generations.py +212 -0
- martini_client-0.1.0/tests/test_golden_v1.py +100 -0
- martini_client-0.1.0/tests/test_overloads_and_polling.py +144 -0
- martini_client-0.1.0/tests/test_workflows.py +168 -0
- martini_client-0.1.0/uv.lock +582 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# `martini-client` (Python) — working notes and release checklist
|
|
2
|
+
|
|
3
|
+
This directory is the independently published PyPI package for the Martini API (generations, workflow runs,
|
|
4
|
+
assets). It is the Python twin of `packages/martini-client` (`@martini-film/client`); both speak the same `/v1`
|
|
5
|
+
contract and both are read as text by the backend's drift guards.
|
|
6
|
+
|
|
7
|
+
## Working here
|
|
8
|
+
|
|
9
|
+
- Toolchain: `uv`. From this directory: `uv sync`, then `uv run pytest`, `uv run mypy`, `uv run ruff check .`,
|
|
10
|
+
`uv run ruff format .`. All four must be green before handing back (`/verify` runs them when this path changed).
|
|
11
|
+
- Python ≥ 3.10; no runtime dependency beyond `httpx`. Do not add one without a reason the README can state.
|
|
12
|
+
- `src/martini_client/endpoints.py` (`ENDPOINTS`) must equal the backend handler registry —
|
|
13
|
+
`backend/src/falCompat/externalAdapters.test.ts` asserts it. When a model is added, add its alias here **and**
|
|
14
|
+
place it in a family in `src/martini_client/_families.py`, then run `uv run python scripts/gen_overloads.py`
|
|
15
|
+
(the `@overload` blocks on `generations.submit()`/`subscribe()` are generated; `tests/test_overloads_and_polling.py`
|
|
16
|
+
fails when they are stale). Add the new input `TypedDict` in `src/martini_client/types/inputs.py`.
|
|
17
|
+
- Every `/v1/generations` route must be called from this package — `backend/src/apiV1/publicContract.test.ts`
|
|
18
|
+
checks it — and the docs pages cite the version in `pyproject.toml`.
|
|
19
|
+
- `tests/test_golden_v1.py` parses `backend/src/apiV1/fixtures/goldenResponses.v1.json`; when the backend
|
|
20
|
+
regenerates that fixture on purpose, this suite says what the Python side no longer understands.
|
|
21
|
+
- Sync and async share everything except the transport (`_http.py`) and the poll driver (`_polling.py`). Add a
|
|
22
|
+
method by writing its `RequestSpec` builder once and calling it from both classes.
|
|
23
|
+
- Docstrings are what agents and IDE hovers read: plain text, the route in backticks, the errors a caller should
|
|
24
|
+
expect. No Sphinx roles.
|
|
25
|
+
|
|
26
|
+
## Releasing
|
|
27
|
+
|
|
28
|
+
`pyproject.toml` `project.version` is the single source of truth (`_version.py` reads installed metadata). PyPI
|
|
29
|
+
versions are immutable.
|
|
30
|
+
|
|
31
|
+
1. Bump `version` in `pyproject.toml` according to semver.
|
|
32
|
+
2. `uv run pytest && uv run mypy && uv run ruff check .`
|
|
33
|
+
3. `uv build`, then inspect `dist/` (`python -m zipfile -l dist/*.whl`): `py.typed`, `LICENSE`, no tests.
|
|
34
|
+
4. Optional dry run: `uv publish --publish-url https://test.pypi.org/legacy/`.
|
|
35
|
+
5. Publish only after the user explicitly authorizes it and a PyPI token is available: `uv publish`.
|
|
36
|
+
6. Update the version the docs pages cite (`ApiGenerationDocs.tsx`, `WorkflowsApiDocs.tsx`) in the same PR — the
|
|
37
|
+
drift guard fails otherwise — and do not merge docs that advertise a version that is not on PyPI yet.
|
|
38
|
+
|
|
39
|
+
Do not publish for changes outside this package, test-only changes, or documentation that does not ship in the
|
|
40
|
+
wheel (README does ship).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Martini
|
|
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.
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: martini-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The official Python client for the Martini API: generations, workflow runs, and asset uploads.
|
|
5
|
+
Project-URL: Homepage, https://www.martini.film
|
|
6
|
+
Project-URL: Documentation, https://www.martini.film/docs/api
|
|
7
|
+
Project-URL: Source, https://github.com/c47-inc/martini/tree/main/packages/martini-python
|
|
8
|
+
Author-email: Martini <support@martini.film>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,generation,martini,sdk,video
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: httpx<1,>=0.27
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# `martini-client`
|
|
26
|
+
|
|
27
|
+
The official Python client for the Martini API: generate images and video, run saved workflows, and upload
|
|
28
|
+
reusable media. Sync and async, fully typed, one dependency (`httpx`).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install martini-client
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Python 3.10 or later.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from martini_client import Martini
|
|
38
|
+
|
|
39
|
+
martini = Martini() # reads MARTINI_API_KEY
|
|
40
|
+
|
|
41
|
+
generation = martini.generations.subscribe(
|
|
42
|
+
"bytedance/seedance-2.5/text-to-video",
|
|
43
|
+
input={
|
|
44
|
+
"prompt": "A practical miniature moon base at blue hour.",
|
|
45
|
+
"duration": "auto",
|
|
46
|
+
"resolution": "720p",
|
|
47
|
+
},
|
|
48
|
+
idempotency_key="shot-42",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
print(generation.result()["video"]["url"], generation.olive_cost) # raises GenerationFailedError if it failed
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`subscribe()` returns whatever the generation settled as — inspect `generation.succeeded`, `.output`, and `.error`
|
|
55
|
+
when you would rather branch than raise; `result()` is the typed accessor (the `concurrent.futures` idiom).
|
|
56
|
+
|
|
57
|
+
## For coding agents
|
|
58
|
+
|
|
59
|
+
The whole contract in one place. If you are an agent integrating Martini, this section is enough.
|
|
60
|
+
|
|
61
|
+
- **Credentials.** `MARTINI_API_KEY` (`mtn_live_…`) in the environment; the key is a server-side secret. Confirm it
|
|
62
|
+
with `martini.me()`. Point at a staging or local API with `MARTINI_API_URL`.
|
|
63
|
+
- **What you can call.** `martini.models()` lists the endpoint aliases this key can invoke, with pricing; pass
|
|
64
|
+
`.endpoint` values to `generations.submit()`. Hard-coding an alias from `martini_client.ENDPOINTS` also works.
|
|
65
|
+
- **One call to generate.** `martini.generations.subscribe(endpoint, input={...})` submits and polls until the
|
|
66
|
+
generation settles. It **returns** a `Generation` whose `status` is `completed`, `failed`, or `cancelled` — it
|
|
67
|
+
does not raise on failure. Call `generation.result()` for the output (typed; raises `GenerationFailedError`
|
|
68
|
+
with the `error` attached if it failed), or inspect `succeeded` / `output` / `error` to branch instead.
|
|
69
|
+
- **Model input** is a plain dict in the model's own field names (`prompt`, `image_url`, `duration`, ...). The
|
|
70
|
+
server validates strictly: an unknown field is `UNSUPPORTED_INPUT`, a bad value `INVALID_INPUT`. The
|
|
71
|
+
`TypedDict`s in `martini_client.types` spell out every field per endpoint; annotate the dict with one to have
|
|
72
|
+
a type checker catch mistakes before the request.
|
|
73
|
+
- **Always pass `idempotency_key`** for anything you might retry; re-submitting with the same key returns the same
|
|
74
|
+
generation instead of charging twice, and it makes the request safe for the client to retry on transport errors.
|
|
75
|
+
- **Errors** are `MartiniAPIError` subclasses keyed by HTTP status (`RateLimitError`, `InsufficientOlivesError`,
|
|
76
|
+
`NotFoundError`, ...), each carrying the stable `code`. Retry only `RateLimitError` (after `retry_after`
|
|
77
|
+
seconds) and `ServerError`; never retry validation, authentication, or balance errors.
|
|
78
|
+
- **Workflows.** `martini.workflows.subscribe(workflow_id, bins={...}, variables={...})` runs a saved workflow and
|
|
79
|
+
returns `WorkflowSettled(run, results)`; `results.ready` are downloadable outputs with presigned `url`s that
|
|
80
|
+
expire after `expires_in` seconds — fetch them promptly.
|
|
81
|
+
- **Media in.** `martini.assets.upload("file.mp4")` returns an `Asset`; put `asset.url` in `image_url`,
|
|
82
|
+
`video_urls`, and so on. Or pass public HTTPS URLs / `data:` URLs directly in those fields.
|
|
83
|
+
|
|
84
|
+
## Generations
|
|
85
|
+
|
|
86
|
+
`martini.generations` is the Martini-native generation API (`/v1/generations`). One resource describes a
|
|
87
|
+
generation from draft to terminal state; `output` is on the same object once `status == "completed"`, so you
|
|
88
|
+
poll one thing.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
submitted = martini.generations.submit(
|
|
92
|
+
"nano-banana-2",
|
|
93
|
+
input={"prompt": "A graphite storyboard frame of a desert observatory.", "resolution": "1K"},
|
|
94
|
+
project_id=project_id, # optional: override the key's default project
|
|
95
|
+
idempotency_key="frame-042", # optional but recommended
|
|
96
|
+
)
|
|
97
|
+
current = martini.generations.status(submitted.id) # output is here once completed
|
|
98
|
+
settled = martini.generations.wait(submitted.id) # poll until completed | failed | cancelled
|
|
99
|
+
cancelled = martini.generations.cancel(submitted.id) # 409 REQUEST_ALREADY_TERMINAL once settled
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`subscribe()` is `submit()` + `wait()`. Both `wait()` and `subscribe()` accept `poll_interval` (seconds, default 2,
|
|
103
|
+
backing off to 15), `timeout` (default 30 minutes; raises `MartiniTimeoutError`, the generation keeps running on
|
|
104
|
+
Martini), and `on_update` (called with every observation).
|
|
105
|
+
|
|
106
|
+
### Drafts
|
|
107
|
+
|
|
108
|
+
`mode="draft"` creates an editable canvas node without charging or contacting a provider. Start it later; the
|
|
109
|
+
run uses the draft's latest settings from the Martini canvas.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
draft = martini.generations.submit("nano-banana-2", input={"prompt": "…"}, mode="draft")
|
|
113
|
+
print(draft.status, draft.links.generate) # "draft", "/v1/generations/<id>/generate"
|
|
114
|
+
started = martini.generations.generate(draft.id) # -> pending
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A draft never settles on its own; `wait()` on one raises `DraftNotStartedError` pointing you at `generate()`.
|
|
118
|
+
|
|
119
|
+
### The `Generation` object
|
|
120
|
+
|
|
121
|
+
| Attribute | Meaning |
|
|
122
|
+
| --- | --- |
|
|
123
|
+
| `id` | Public request id; also the canvas asset the generation lands on. |
|
|
124
|
+
| `endpoint` | The alias it was submitted against. |
|
|
125
|
+
| `status` | `draft`, `pending`, `running`, `completed`, `failed`, or `cancelled`. `is_terminal`, `succeeded` are convenience properties. |
|
|
126
|
+
| `output` | The model's output in its fal field names once completed (`output["video"]["url"]`, `output["images"][0]["url"]`); otherwise `None`. URLs are durable Martini storage URLs. `result()` returns it typed, or raises. |
|
|
127
|
+
| `error` | `ErrorDetail(code, message)` when failed or cancelled; otherwise `None`. |
|
|
128
|
+
| `olive_cost` | Snapshotted charge in olives; `None` only while a concurrent idempotent submit is still being priced. |
|
|
129
|
+
| `warnings` | Non-blocking input advisories (unbound reference images). The generation was still accepted. |
|
|
130
|
+
| `martini` | `GenerationPlacement(preview_url, project_id, canvas_id, asset_id)` — open `preview_url` to see it in Martini. |
|
|
131
|
+
| `created_at`, `started_at`, `completed_at` | ISO timestamps. |
|
|
132
|
+
|
|
133
|
+
### Typed inputs
|
|
134
|
+
|
|
135
|
+
With a literal endpoint, `submit()`/`subscribe()` narrow `input` to that endpoint's `TypedDict` (your editor
|
|
136
|
+
completes the fields) and type the returned `output`. To have `mypy`/`pyright` reject a misspelled field or an
|
|
137
|
+
out-of-range value as well, annotate the input with its `TypedDict` — an unannotated dict literal is accepted
|
|
138
|
+
as a plain mapping, and the server rejects it at submit time instead (`UNSUPPORTED_INPUT`, `INVALID_INPUT`):
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from martini_client.types import Seedance25ReferenceToVideoInput
|
|
142
|
+
|
|
143
|
+
input: Seedance25ReferenceToVideoInput = {
|
|
144
|
+
"prompt": "@Image1 is the lead. Inside @Image2, @Image1 turns to camera. Follow the move from @Video1.",
|
|
145
|
+
"image_urls": [character_url, location_url],
|
|
146
|
+
"video_urls": [camera_guide_url],
|
|
147
|
+
"duration": "8",
|
|
148
|
+
"resolution": "720p",
|
|
149
|
+
}
|
|
150
|
+
generation = martini.generations.subscribe("bytedance/seedance-2.5/reference-to-video", input=input)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Reference arrays are positional: `image_urls[0]` is `@Image1`, `video_urls[0]` is `@Video1`, `audio_urls[0]` is
|
|
154
|
+
`@Audio1`. Bare names do not bind media. Multiple video references and every audio reference must be bound;
|
|
155
|
+
an unbound image is accepted with an advisory in `warnings`.
|
|
156
|
+
|
|
157
|
+
### Discovering models
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
catalog = martini.models()
|
|
161
|
+
for model in catalog.with_capability("generate-video"):
|
|
162
|
+
print(model.endpoint, model.billable_unit, model.model.pricing.display, model.pricing)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Only aliases your key can invoke are listed, with your organization's negotiated multipliers when any.
|
|
166
|
+
|
|
167
|
+
## Workflows and runs
|
|
168
|
+
|
|
169
|
+
A **saved workflow** is your workspace's reusable, versioned machinery; a **placed workflow** is a copy on a
|
|
170
|
+
canvas. A **run** is one execution.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
workflows = martini.workflows.list() # saved; .list(all=True) adds placed ones
|
|
174
|
+
workflow = workflows.find("Script to Video") # by name (case-insensitive) or id
|
|
175
|
+
print(workflow.inputs.bins, workflow.inputs.variables)
|
|
176
|
+
|
|
177
|
+
script = martini.assets.upload("script.pdf.png") # inputs are assets; upload first
|
|
178
|
+
|
|
179
|
+
settled = martini.workflows.subscribe(
|
|
180
|
+
workflow.id,
|
|
181
|
+
bins={"Script": [script.asset_id]}, # a saved workflow starts from empty bins: pin every input bin
|
|
182
|
+
variables={"Tone": "deadpan"},
|
|
183
|
+
fingerprint=workflow.fingerprint, # refuse to run if the machinery changed since you read it
|
|
184
|
+
idempotency_key="job-7",
|
|
185
|
+
)
|
|
186
|
+
for output in settled.results.ready:
|
|
187
|
+
print(output.filename, output.url) # presigned; valid for output.expires_in seconds
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`workflows.run()` starts without waiting; `runs.status(run_id)`, `runs.results(run_id)` (409
|
|
191
|
+
`WORKFLOW_RUN_NOT_SETTLED` until the run settles), and `runs.wait(run_id)` poll an existing run. `subscribe()` and
|
|
192
|
+
`wait()` return a `failed` or `cancelled` run with its `error` rather than raising; `.raise_for_status()` turns
|
|
193
|
+
it into a `WorkflowRunFailedError`. `olive_budget` caps generation spend; omit it to let Martini arm the rail from
|
|
194
|
+
the estimate. Runs started here auto-approve.
|
|
195
|
+
|
|
196
|
+
## Assets
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
asset = martini.assets.upload("reference.mp4") # path, bytes, or a binary file object
|
|
200
|
+
asset = martini.assets.upload(data, filename="ref.png", content_type="image/png")
|
|
201
|
+
asset = martini.assets.upload("ref.png", project_id=..., canvas_id=...) # another project you can edit
|
|
202
|
+
asset = martini.assets.upload("ref.png", wait=False) # return after complete; status queued/processing
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`upload()` is prepare → PUT directly to storage → complete → poll until processed. Limits: 10 MB JPEG/PNG/WebP,
|
|
206
|
+
30 MB MP4, 15 MB MP3/WAV. When direct storage is unavailable it raises `StorageUnavailableError` — it does not
|
|
207
|
+
switch transports on its own; `martini.assets.upload_base64(...)` (same arguments) is the explicit alternative.
|
|
208
|
+
The lower-level steps are exposed too: `prepare_upload(files)`, `complete_upload(asset_ids)`, `get(asset_id)`,
|
|
209
|
+
`wait(asset_id)`.
|
|
210
|
+
|
|
211
|
+
## Errors
|
|
212
|
+
|
|
213
|
+
Every non-2xx answer raises a subclass of `MartiniAPIError` chosen by HTTP status; branch on the class, then on
|
|
214
|
+
the stable `code` when you need the exact cause.
|
|
215
|
+
|
|
216
|
+
| Class | Status | Typical codes |
|
|
217
|
+
| --- | --- | --- |
|
|
218
|
+
| `InvalidRequestError` | 400, 422 | `INVALID_REQUEST`, `INVALID_INPUT`, `UNSUPPORTED_INPUT`, `INVALID_MEDIA`, `MEDIA_TOO_LARGE`, `WORKFLOW_BIN_REQUIRED`, `WORKFLOW_VARIABLE_NOT_FOUND` |
|
|
219
|
+
| `AuthenticationError` | 401 | `INVALID_API_KEY`, `API_KEY_REVOKED` |
|
|
220
|
+
| `InsufficientOlivesError` | 402 | `INSUFFICIENT_OLIVES`, `SPEND_CAP_EXCEEDED` |
|
|
221
|
+
| `AccessDeniedError` | 403 | `PROJECT_ACCESS_DENIED`, `GENERATION_API_DISABLED`, `WORKFLOWS_NOT_ENABLED` |
|
|
222
|
+
| `NotFoundError` | 404 | `ENDPOINT_NOT_REGISTERED`, `REQUEST_NOT_FOUND`, `CANVAS_NOT_FOUND`, `WORKFLOW_NOT_FOUND`, `WORKFLOW_RUN_NOT_FOUND` |
|
|
223
|
+
| `ConflictError` | 409 | `REQUEST_ALREADY_TERMINAL`, `DRAFT_ALREADY_GENERATING`, `WORKFLOW_RUN_NOT_SETTLED`, `WORKFLOW_CHANGED`, `PROJECT_DOCUMENT_TOO_LARGE` |
|
|
224
|
+
| `RateLimitError` | 429 | `RATE_LIMITED` — `retry_after` carries the server's hint in seconds |
|
|
225
|
+
| `ServerError` | 5xx | `GENERATION_SUBMISSION_FAILED`, `GENERATION_API_ACCESS_UNAVAILABLE` |
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from martini_client import ConflictError, RateLimitError
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
results = martini.runs.results(run_id)
|
|
232
|
+
except ConflictError as error:
|
|
233
|
+
if error.code == "WORKFLOW_RUN_NOT_SETTLED":
|
|
234
|
+
... # keep polling
|
|
235
|
+
except RateLimitError as error:
|
|
236
|
+
time.sleep(error.retry_after or 10)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`error.body` holds the full JSON for fields beyond `error`/`code` (the `inputs` a workflow accepts, the current
|
|
240
|
+
`fingerprint`). Outside the API: `MartiniTransportError` (no response after retries), `MartiniTimeoutError`
|
|
241
|
+
(a `wait`/`subscribe` outlasted `timeout`), `AssetUploadError`/`StorageUnavailableError` (uploads),
|
|
242
|
+
`GenerationFailedError`/`WorkflowRunFailedError` (from `raise_for_status()`), `ConfigurationError` (bad
|
|
243
|
+
constructor arguments). All derive from `MartiniError`.
|
|
244
|
+
|
|
245
|
+
`PROJECT_DOCUMENT_TOO_LARGE` (409) is durable, not transient: the project cannot generate until support rebuilds
|
|
246
|
+
its document. Surface the message and stop.
|
|
247
|
+
|
|
248
|
+
## Configuration
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
martini = Martini(
|
|
252
|
+
api_key="mtn_live_…", # default: MARTINI_API_KEY
|
|
253
|
+
base_url="https://api.martini.film", # default: MARTINI_API_URL, then production; HTTPS except localhost
|
|
254
|
+
api_version="2026-08-13", # X-Martini-API-Version; default is the version this release targets
|
|
255
|
+
timeout=60.0, # seconds per request
|
|
256
|
+
max_retries=2, # for GETs and idempotent submits, on transport errors / 429 / 5xx
|
|
257
|
+
http_client=httpx.Client(...), # optional: proxies, custom transports; you own its lifetime
|
|
258
|
+
)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Retries back off exponentially with jitter (0.5 s, 1 s, 2 s, … capped at 8 s) and honour `Retry-After`. A
|
|
262
|
+
`POST` without an `idempotency_key` is never retried automatically — the server could not tell a retry from a
|
|
263
|
+
second request. `with Martini() as martini:` closes the connection pool on exit.
|
|
264
|
+
|
|
265
|
+
### Async
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from martini_client import AsyncMartini
|
|
269
|
+
|
|
270
|
+
async with AsyncMartini() as martini:
|
|
271
|
+
generation = await martini.generations.subscribe("nano-banana-2", input={"prompt": "…"})
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Same methods, same arguments, `await`ed.
|
|
275
|
+
|
|
276
|
+
## Supported methods
|
|
277
|
+
|
|
278
|
+
- `me()`, `models()`
|
|
279
|
+
- `generations.submit()`, `.status()`, `.cancel()`, `.generate()`, `.wait()`, `.subscribe()`
|
|
280
|
+
- `workflows.list()`, `.get()`, `.create()`, `.run()`, `.subscribe()`
|
|
281
|
+
- `runs.status()`, `.results()`, `.wait()`
|
|
282
|
+
- `assets.upload()`, `.upload_base64()`, `.prepare_upload()`, `.complete_upload()`, `.get()`, `.wait()`
|
|
283
|
+
|
|
284
|
+
Wire reference: [Generation API](https://www.martini.film/docs/api) and
|
|
285
|
+
[Workflows API](https://www.martini.film/docs/workflows). The JavaScript client is
|
|
286
|
+
[`@martini-film/client`](https://www.npmjs.com/package/@martini-film/client).
|
|
287
|
+
|
|
288
|
+
The API key is a server-side secret. Do not embed it in notebooks you share, client-side code, or public
|
|
289
|
+
environment variables.
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# `martini-client`
|
|
2
|
+
|
|
3
|
+
The official Python client for the Martini API: generate images and video, run saved workflows, and upload
|
|
4
|
+
reusable media. Sync and async, fully typed, one dependency (`httpx`).
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install martini-client
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requires Python 3.10 or later.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from martini_client import Martini
|
|
14
|
+
|
|
15
|
+
martini = Martini() # reads MARTINI_API_KEY
|
|
16
|
+
|
|
17
|
+
generation = martini.generations.subscribe(
|
|
18
|
+
"bytedance/seedance-2.5/text-to-video",
|
|
19
|
+
input={
|
|
20
|
+
"prompt": "A practical miniature moon base at blue hour.",
|
|
21
|
+
"duration": "auto",
|
|
22
|
+
"resolution": "720p",
|
|
23
|
+
},
|
|
24
|
+
idempotency_key="shot-42",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
print(generation.result()["video"]["url"], generation.olive_cost) # raises GenerationFailedError if it failed
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`subscribe()` returns whatever the generation settled as — inspect `generation.succeeded`, `.output`, and `.error`
|
|
31
|
+
when you would rather branch than raise; `result()` is the typed accessor (the `concurrent.futures` idiom).
|
|
32
|
+
|
|
33
|
+
## For coding agents
|
|
34
|
+
|
|
35
|
+
The whole contract in one place. If you are an agent integrating Martini, this section is enough.
|
|
36
|
+
|
|
37
|
+
- **Credentials.** `MARTINI_API_KEY` (`mtn_live_…`) in the environment; the key is a server-side secret. Confirm it
|
|
38
|
+
with `martini.me()`. Point at a staging or local API with `MARTINI_API_URL`.
|
|
39
|
+
- **What you can call.** `martini.models()` lists the endpoint aliases this key can invoke, with pricing; pass
|
|
40
|
+
`.endpoint` values to `generations.submit()`. Hard-coding an alias from `martini_client.ENDPOINTS` also works.
|
|
41
|
+
- **One call to generate.** `martini.generations.subscribe(endpoint, input={...})` submits and polls until the
|
|
42
|
+
generation settles. It **returns** a `Generation` whose `status` is `completed`, `failed`, or `cancelled` — it
|
|
43
|
+
does not raise on failure. Call `generation.result()` for the output (typed; raises `GenerationFailedError`
|
|
44
|
+
with the `error` attached if it failed), or inspect `succeeded` / `output` / `error` to branch instead.
|
|
45
|
+
- **Model input** is a plain dict in the model's own field names (`prompt`, `image_url`, `duration`, ...). The
|
|
46
|
+
server validates strictly: an unknown field is `UNSUPPORTED_INPUT`, a bad value `INVALID_INPUT`. The
|
|
47
|
+
`TypedDict`s in `martini_client.types` spell out every field per endpoint; annotate the dict with one to have
|
|
48
|
+
a type checker catch mistakes before the request.
|
|
49
|
+
- **Always pass `idempotency_key`** for anything you might retry; re-submitting with the same key returns the same
|
|
50
|
+
generation instead of charging twice, and it makes the request safe for the client to retry on transport errors.
|
|
51
|
+
- **Errors** are `MartiniAPIError` subclasses keyed by HTTP status (`RateLimitError`, `InsufficientOlivesError`,
|
|
52
|
+
`NotFoundError`, ...), each carrying the stable `code`. Retry only `RateLimitError` (after `retry_after`
|
|
53
|
+
seconds) and `ServerError`; never retry validation, authentication, or balance errors.
|
|
54
|
+
- **Workflows.** `martini.workflows.subscribe(workflow_id, bins={...}, variables={...})` runs a saved workflow and
|
|
55
|
+
returns `WorkflowSettled(run, results)`; `results.ready` are downloadable outputs with presigned `url`s that
|
|
56
|
+
expire after `expires_in` seconds — fetch them promptly.
|
|
57
|
+
- **Media in.** `martini.assets.upload("file.mp4")` returns an `Asset`; put `asset.url` in `image_url`,
|
|
58
|
+
`video_urls`, and so on. Or pass public HTTPS URLs / `data:` URLs directly in those fields.
|
|
59
|
+
|
|
60
|
+
## Generations
|
|
61
|
+
|
|
62
|
+
`martini.generations` is the Martini-native generation API (`/v1/generations`). One resource describes a
|
|
63
|
+
generation from draft to terminal state; `output` is on the same object once `status == "completed"`, so you
|
|
64
|
+
poll one thing.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
submitted = martini.generations.submit(
|
|
68
|
+
"nano-banana-2",
|
|
69
|
+
input={"prompt": "A graphite storyboard frame of a desert observatory.", "resolution": "1K"},
|
|
70
|
+
project_id=project_id, # optional: override the key's default project
|
|
71
|
+
idempotency_key="frame-042", # optional but recommended
|
|
72
|
+
)
|
|
73
|
+
current = martini.generations.status(submitted.id) # output is here once completed
|
|
74
|
+
settled = martini.generations.wait(submitted.id) # poll until completed | failed | cancelled
|
|
75
|
+
cancelled = martini.generations.cancel(submitted.id) # 409 REQUEST_ALREADY_TERMINAL once settled
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`subscribe()` is `submit()` + `wait()`. Both `wait()` and `subscribe()` accept `poll_interval` (seconds, default 2,
|
|
79
|
+
backing off to 15), `timeout` (default 30 minutes; raises `MartiniTimeoutError`, the generation keeps running on
|
|
80
|
+
Martini), and `on_update` (called with every observation).
|
|
81
|
+
|
|
82
|
+
### Drafts
|
|
83
|
+
|
|
84
|
+
`mode="draft"` creates an editable canvas node without charging or contacting a provider. Start it later; the
|
|
85
|
+
run uses the draft's latest settings from the Martini canvas.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
draft = martini.generations.submit("nano-banana-2", input={"prompt": "…"}, mode="draft")
|
|
89
|
+
print(draft.status, draft.links.generate) # "draft", "/v1/generations/<id>/generate"
|
|
90
|
+
started = martini.generations.generate(draft.id) # -> pending
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A draft never settles on its own; `wait()` on one raises `DraftNotStartedError` pointing you at `generate()`.
|
|
94
|
+
|
|
95
|
+
### The `Generation` object
|
|
96
|
+
|
|
97
|
+
| Attribute | Meaning |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| `id` | Public request id; also the canvas asset the generation lands on. |
|
|
100
|
+
| `endpoint` | The alias it was submitted against. |
|
|
101
|
+
| `status` | `draft`, `pending`, `running`, `completed`, `failed`, or `cancelled`. `is_terminal`, `succeeded` are convenience properties. |
|
|
102
|
+
| `output` | The model's output in its fal field names once completed (`output["video"]["url"]`, `output["images"][0]["url"]`); otherwise `None`. URLs are durable Martini storage URLs. `result()` returns it typed, or raises. |
|
|
103
|
+
| `error` | `ErrorDetail(code, message)` when failed or cancelled; otherwise `None`. |
|
|
104
|
+
| `olive_cost` | Snapshotted charge in olives; `None` only while a concurrent idempotent submit is still being priced. |
|
|
105
|
+
| `warnings` | Non-blocking input advisories (unbound reference images). The generation was still accepted. |
|
|
106
|
+
| `martini` | `GenerationPlacement(preview_url, project_id, canvas_id, asset_id)` — open `preview_url` to see it in Martini. |
|
|
107
|
+
| `created_at`, `started_at`, `completed_at` | ISO timestamps. |
|
|
108
|
+
|
|
109
|
+
### Typed inputs
|
|
110
|
+
|
|
111
|
+
With a literal endpoint, `submit()`/`subscribe()` narrow `input` to that endpoint's `TypedDict` (your editor
|
|
112
|
+
completes the fields) and type the returned `output`. To have `mypy`/`pyright` reject a misspelled field or an
|
|
113
|
+
out-of-range value as well, annotate the input with its `TypedDict` — an unannotated dict literal is accepted
|
|
114
|
+
as a plain mapping, and the server rejects it at submit time instead (`UNSUPPORTED_INPUT`, `INVALID_INPUT`):
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from martini_client.types import Seedance25ReferenceToVideoInput
|
|
118
|
+
|
|
119
|
+
input: Seedance25ReferenceToVideoInput = {
|
|
120
|
+
"prompt": "@Image1 is the lead. Inside @Image2, @Image1 turns to camera. Follow the move from @Video1.",
|
|
121
|
+
"image_urls": [character_url, location_url],
|
|
122
|
+
"video_urls": [camera_guide_url],
|
|
123
|
+
"duration": "8",
|
|
124
|
+
"resolution": "720p",
|
|
125
|
+
}
|
|
126
|
+
generation = martini.generations.subscribe("bytedance/seedance-2.5/reference-to-video", input=input)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Reference arrays are positional: `image_urls[0]` is `@Image1`, `video_urls[0]` is `@Video1`, `audio_urls[0]` is
|
|
130
|
+
`@Audio1`. Bare names do not bind media. Multiple video references and every audio reference must be bound;
|
|
131
|
+
an unbound image is accepted with an advisory in `warnings`.
|
|
132
|
+
|
|
133
|
+
### Discovering models
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
catalog = martini.models()
|
|
137
|
+
for model in catalog.with_capability("generate-video"):
|
|
138
|
+
print(model.endpoint, model.billable_unit, model.model.pricing.display, model.pricing)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Only aliases your key can invoke are listed, with your organization's negotiated multipliers when any.
|
|
142
|
+
|
|
143
|
+
## Workflows and runs
|
|
144
|
+
|
|
145
|
+
A **saved workflow** is your workspace's reusable, versioned machinery; a **placed workflow** is a copy on a
|
|
146
|
+
canvas. A **run** is one execution.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
workflows = martini.workflows.list() # saved; .list(all=True) adds placed ones
|
|
150
|
+
workflow = workflows.find("Script to Video") # by name (case-insensitive) or id
|
|
151
|
+
print(workflow.inputs.bins, workflow.inputs.variables)
|
|
152
|
+
|
|
153
|
+
script = martini.assets.upload("script.pdf.png") # inputs are assets; upload first
|
|
154
|
+
|
|
155
|
+
settled = martini.workflows.subscribe(
|
|
156
|
+
workflow.id,
|
|
157
|
+
bins={"Script": [script.asset_id]}, # a saved workflow starts from empty bins: pin every input bin
|
|
158
|
+
variables={"Tone": "deadpan"},
|
|
159
|
+
fingerprint=workflow.fingerprint, # refuse to run if the machinery changed since you read it
|
|
160
|
+
idempotency_key="job-7",
|
|
161
|
+
)
|
|
162
|
+
for output in settled.results.ready:
|
|
163
|
+
print(output.filename, output.url) # presigned; valid for output.expires_in seconds
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`workflows.run()` starts without waiting; `runs.status(run_id)`, `runs.results(run_id)` (409
|
|
167
|
+
`WORKFLOW_RUN_NOT_SETTLED` until the run settles), and `runs.wait(run_id)` poll an existing run. `subscribe()` and
|
|
168
|
+
`wait()` return a `failed` or `cancelled` run with its `error` rather than raising; `.raise_for_status()` turns
|
|
169
|
+
it into a `WorkflowRunFailedError`. `olive_budget` caps generation spend; omit it to let Martini arm the rail from
|
|
170
|
+
the estimate. Runs started here auto-approve.
|
|
171
|
+
|
|
172
|
+
## Assets
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
asset = martini.assets.upload("reference.mp4") # path, bytes, or a binary file object
|
|
176
|
+
asset = martini.assets.upload(data, filename="ref.png", content_type="image/png")
|
|
177
|
+
asset = martini.assets.upload("ref.png", project_id=..., canvas_id=...) # another project you can edit
|
|
178
|
+
asset = martini.assets.upload("ref.png", wait=False) # return after complete; status queued/processing
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`upload()` is prepare → PUT directly to storage → complete → poll until processed. Limits: 10 MB JPEG/PNG/WebP,
|
|
182
|
+
30 MB MP4, 15 MB MP3/WAV. When direct storage is unavailable it raises `StorageUnavailableError` — it does not
|
|
183
|
+
switch transports on its own; `martini.assets.upload_base64(...)` (same arguments) is the explicit alternative.
|
|
184
|
+
The lower-level steps are exposed too: `prepare_upload(files)`, `complete_upload(asset_ids)`, `get(asset_id)`,
|
|
185
|
+
`wait(asset_id)`.
|
|
186
|
+
|
|
187
|
+
## Errors
|
|
188
|
+
|
|
189
|
+
Every non-2xx answer raises a subclass of `MartiniAPIError` chosen by HTTP status; branch on the class, then on
|
|
190
|
+
the stable `code` when you need the exact cause.
|
|
191
|
+
|
|
192
|
+
| Class | Status | Typical codes |
|
|
193
|
+
| --- | --- | --- |
|
|
194
|
+
| `InvalidRequestError` | 400, 422 | `INVALID_REQUEST`, `INVALID_INPUT`, `UNSUPPORTED_INPUT`, `INVALID_MEDIA`, `MEDIA_TOO_LARGE`, `WORKFLOW_BIN_REQUIRED`, `WORKFLOW_VARIABLE_NOT_FOUND` |
|
|
195
|
+
| `AuthenticationError` | 401 | `INVALID_API_KEY`, `API_KEY_REVOKED` |
|
|
196
|
+
| `InsufficientOlivesError` | 402 | `INSUFFICIENT_OLIVES`, `SPEND_CAP_EXCEEDED` |
|
|
197
|
+
| `AccessDeniedError` | 403 | `PROJECT_ACCESS_DENIED`, `GENERATION_API_DISABLED`, `WORKFLOWS_NOT_ENABLED` |
|
|
198
|
+
| `NotFoundError` | 404 | `ENDPOINT_NOT_REGISTERED`, `REQUEST_NOT_FOUND`, `CANVAS_NOT_FOUND`, `WORKFLOW_NOT_FOUND`, `WORKFLOW_RUN_NOT_FOUND` |
|
|
199
|
+
| `ConflictError` | 409 | `REQUEST_ALREADY_TERMINAL`, `DRAFT_ALREADY_GENERATING`, `WORKFLOW_RUN_NOT_SETTLED`, `WORKFLOW_CHANGED`, `PROJECT_DOCUMENT_TOO_LARGE` |
|
|
200
|
+
| `RateLimitError` | 429 | `RATE_LIMITED` — `retry_after` carries the server's hint in seconds |
|
|
201
|
+
| `ServerError` | 5xx | `GENERATION_SUBMISSION_FAILED`, `GENERATION_API_ACCESS_UNAVAILABLE` |
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from martini_client import ConflictError, RateLimitError
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
results = martini.runs.results(run_id)
|
|
208
|
+
except ConflictError as error:
|
|
209
|
+
if error.code == "WORKFLOW_RUN_NOT_SETTLED":
|
|
210
|
+
... # keep polling
|
|
211
|
+
except RateLimitError as error:
|
|
212
|
+
time.sleep(error.retry_after or 10)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`error.body` holds the full JSON for fields beyond `error`/`code` (the `inputs` a workflow accepts, the current
|
|
216
|
+
`fingerprint`). Outside the API: `MartiniTransportError` (no response after retries), `MartiniTimeoutError`
|
|
217
|
+
(a `wait`/`subscribe` outlasted `timeout`), `AssetUploadError`/`StorageUnavailableError` (uploads),
|
|
218
|
+
`GenerationFailedError`/`WorkflowRunFailedError` (from `raise_for_status()`), `ConfigurationError` (bad
|
|
219
|
+
constructor arguments). All derive from `MartiniError`.
|
|
220
|
+
|
|
221
|
+
`PROJECT_DOCUMENT_TOO_LARGE` (409) is durable, not transient: the project cannot generate until support rebuilds
|
|
222
|
+
its document. Surface the message and stop.
|
|
223
|
+
|
|
224
|
+
## Configuration
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
martini = Martini(
|
|
228
|
+
api_key="mtn_live_…", # default: MARTINI_API_KEY
|
|
229
|
+
base_url="https://api.martini.film", # default: MARTINI_API_URL, then production; HTTPS except localhost
|
|
230
|
+
api_version="2026-08-13", # X-Martini-API-Version; default is the version this release targets
|
|
231
|
+
timeout=60.0, # seconds per request
|
|
232
|
+
max_retries=2, # for GETs and idempotent submits, on transport errors / 429 / 5xx
|
|
233
|
+
http_client=httpx.Client(...), # optional: proxies, custom transports; you own its lifetime
|
|
234
|
+
)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Retries back off exponentially with jitter (0.5 s, 1 s, 2 s, … capped at 8 s) and honour `Retry-After`. A
|
|
238
|
+
`POST` without an `idempotency_key` is never retried automatically — the server could not tell a retry from a
|
|
239
|
+
second request. `with Martini() as martini:` closes the connection pool on exit.
|
|
240
|
+
|
|
241
|
+
### Async
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from martini_client import AsyncMartini
|
|
245
|
+
|
|
246
|
+
async with AsyncMartini() as martini:
|
|
247
|
+
generation = await martini.generations.subscribe("nano-banana-2", input={"prompt": "…"})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Same methods, same arguments, `await`ed.
|
|
251
|
+
|
|
252
|
+
## Supported methods
|
|
253
|
+
|
|
254
|
+
- `me()`, `models()`
|
|
255
|
+
- `generations.submit()`, `.status()`, `.cancel()`, `.generate()`, `.wait()`, `.subscribe()`
|
|
256
|
+
- `workflows.list()`, `.get()`, `.create()`, `.run()`, `.subscribe()`
|
|
257
|
+
- `runs.status()`, `.results()`, `.wait()`
|
|
258
|
+
- `assets.upload()`, `.upload_base64()`, `.prepare_upload()`, `.complete_upload()`, `.get()`, `.wait()`
|
|
259
|
+
|
|
260
|
+
Wire reference: [Generation API](https://www.martini.film/docs/api) and
|
|
261
|
+
[Workflows API](https://www.martini.film/docs/workflows). The JavaScript client is
|
|
262
|
+
[`@martini-film/client`](https://www.npmjs.com/package/@martini-film/client).
|
|
263
|
+
|
|
264
|
+
The API key is a server-side secret. Do not embed it in notebooks you share, client-side code, or public
|
|
265
|
+
environment variables.
|