mnml-ai 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,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ .DS_Store
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## 0.1.0 — unreleased
8
+
9
+ First release.
10
+
11
+ - Speaks API v2: every call is under `/v2` (`/v1` is now the mnmlai.dev API, which the SDK
12
+ does not call). `uploads.create`, `upload_id` and `mask_upload_id` are gone, as the API no
13
+ longer has uploads: send the image in the call, or a finished `job_id`.
14
+ - A create call's `status` is typed `JobStatus`, as the API now promises (it once answered
15
+ the raw stored status on an idempotent replay). `spec/openapi.json` refreshed (Oct 18 2026).
16
+ - `jobs.stream(job_id)`: follow a job over one connection (Server-Sent Events), as a generator of
17
+ `JobStreamEvent`s: `job` as it changes, then `done`, `timeout` or `error`. `Mnml` takes a
18
+ `stream_transport` for it, as it takes `transport`.
19
+
20
+ - `image` on `renders`, `edits`, `enhancements` and `videos`: send the image in the call, with no
21
+ upload step. A `str` (a link, a data URI or base64) is sent as it is; bytes, a `Path` or an open
22
+ binary file is sent as a base64 data URI, as JSON.
23
+ - `references` items and `end_frame` take an image the same way, or a dict with `image`.
24
+ - `mask` on `edits`: a PNG mask in the call, white to change and black to keep.
25
+ - `image_url` still works and is deprecated: it is the old name of `image`.
26
+ - `wait` (1-90 seconds) on `renders.create` and `jobs.get`: the API holds the answer until the job
27
+ settles. `create_and_wait` and `jobs.wait` use it, so a render is one or two calls, not a polling
28
+ loop.
29
+ - `Mnml` client for the mnml API v2: renders, edits, enhancements, videos, jobs
30
+ (`get`, `wait`, `wait_all`, `cancel`), account and engines.
31
+ - Every request field as a typed keyword argument; a field the API does not take is a `TypeError`.
32
+ - `create_and_wait` on renders, edits, enhancements and videos: start a job and wait for it.
33
+ - `files.download(output, to=None)`: a job output's bytes and content type, from its signed link,
34
+ without sending the key.
35
+ - `TypedDict`s for every answer, the nested ones included (engine prices and capabilities, video
36
+ prices, account limits), and `Literal`s for the values the API takes: engine and video model ids,
37
+ camera moves, motion, modes, aspect ratios.
38
+ - `WebhookEvent` as a union that narrows `data` on `type` (`job.*`, `credits.low`, `webhook.test`).
39
+ - A migration table from the mnmlai.dev routes to SDK calls.
40
+ - Retries for 429, 5xx and dropped connections, with `Retry-After` up to a minute.
41
+ - An `Idempotency-Key` on every spending call, kept across retries.
42
+ - `MnmlError` with the API's `code`, `status`, `request_id` and validation `issues`.
43
+ - `verify_webhook` and `sign_webhook` (Standard Webhooks).
44
+ - Typed answers in `mnml_ai.types`, and `py.typed`. No dependencies, Python 3.9+.
mnml_ai-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mnml
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.
mnml_ai-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,338 @@
1
+ Metadata-Version: 2.5
2
+ Name: mnml-ai
3
+ Version: 0.1.0
4
+ Summary: The official Python client for the mnml API: architecture renders, edits, enhancements and video.
5
+ Project-URL: Homepage, https://developers.mnml.ai/sdks
6
+ Project-URL: Documentation, https://developers.mnml.ai/docs
7
+ Project-URL: Source, https://github.com/mnml-ai/mnml-sdk-python
8
+ Project-URL: Issues, https://github.com/mnml-ai/mnml-sdk-python/issues
9
+ Project-URL: Changelog, https://github.com/mnml-ai/mnml-sdk-python/blob/main/CHANGELOG.md
10
+ Author-email: mnml <hello@mnml.ai>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: ai,api,architecture,mnml,rendering,sdk
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Multimedia :: Graphics
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.2; extra == 'dev'
29
+ Requires-Dist: mypy>=1.11; extra == 'dev'
30
+ Requires-Dist: pytest>=8; extra == 'dev'
31
+ Requires-Dist: ruff>=0.6; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # mnml Python SDK
35
+
36
+ The official Python client for the [mnml API](https://developers.mnml.ai): architecture
37
+ renders, edits, enhancements and video, from your own product.
38
+
39
+ - Every endpoint of API v2: renders, edits, enhancements, video, jobs, account, engines
40
+ - Images sent in the call itself: a link, bytes, a `Path` or an open file, no upload step
41
+ - Retries, timeouts and idempotency keys handled for you
42
+ - `create_and_wait()` and `jobs.wait()` to poll a job until it settles, `files.download()` to keep
43
+ its output
44
+ - Webhook signature verification, with typed events
45
+ - Typed keyword arguments and answers (`TypedDict`), checked against the API's OpenAPI document
46
+ - No dependencies. Python 3.9+
47
+
48
+ ```bash
49
+ pip install mnml-ai
50
+ ```
51
+
52
+ ## Quick start
53
+
54
+ Create an API key in the [console](https://developers.mnml.ai/console/keys), then:
55
+
56
+ ```python
57
+ from pathlib import Path
58
+
59
+ from mnml_ai import Mnml
60
+
61
+ mnml = Mnml() # reads MNML_API_KEY
62
+
63
+ job = mnml.renders.create_and_wait(
64
+ mode="exterior",
65
+ image=Path("massing.png"), # or a link: "https://example.com/massing.png"
66
+ prompt="Timber facade, late afternoon light, olive trees",
67
+ )[0]
68
+ print(job["status"], job["outputs"][0]["url"] if job["outputs"] else None)
69
+ ```
70
+
71
+ Every call spends credits from your prepaid API balance. Top it up in dollars on
72
+ [Billing](https://developers.mnml.ai/console/billing).
73
+
74
+ ## Configuration
75
+
76
+ ```python
77
+ mnml = Mnml(
78
+ api_key=None, # defaults to the MNML_API_KEY environment variable
79
+ max_retries=2, # retries for a 429, a 5xx or a dropped connection
80
+ timeout=60.0, # seconds, per request
81
+ )
82
+ ```
83
+
84
+ ## Methods
85
+
86
+ | Method | Endpoint | What it does |
87
+ | --------------------------------- | --------------------------- | --------------------------------------------------- |
88
+ | `renders.create(...)` | `POST /v2/renders` | Render from an image, or from a prompt alone |
89
+ | `edits.create(...)` | `POST /v2/edits` | Edit or erase, over the whole image or a region |
90
+ | `enhancements.create(...)` | `POST /v2/enhancements` | Upscale, enhance, remove the background, outpaint |
91
+ | `videos.create(...)` | `POST /v2/videos` | Video from a still |
92
+ | `jobs.get(id)` | `GET /v2/jobs/{id}` | Read a job |
93
+ | `jobs.cancel(id)` | `POST /v2/jobs/{id}/cancel` | Cancel a job (refunded if it had not produced yet) |
94
+ | `account.get()` | `GET /v2/account` | Balance, tier and limits for this key |
95
+ | `engines.list()` | `GET /v2/engines` | Engines, video models, prices and capabilities |
96
+ | `files.download(output, to=None)` | `GET /v2/files/{id}` | A job output's bytes, from its signed link |
97
+ | `jobs.wait(id, ...)` | `GET /v2/jobs/{id}` | Read a job until it succeeds, fails or is cancelled |
98
+ | `jobs.wait_all(ids, ...)` | `GET /v2/jobs/{id}` | `wait` for several jobs at once |
99
+ | `jobs.stream(id)` | `GET /v2/jobs/{id}/events` | Follow a job as it changes, over one connection |
100
+ | `<resource>.create_and_wait(...)` | the create, then the job | Start a job and wait for it (a render: every job) |
101
+
102
+ Create calls take the API's fields as keyword arguments, exactly as the
103
+ [reference](https://developers.mnml.ai/docs/renders) names them, typed so your editor completes
104
+ them, and every call returns the answer's `data` as a `dict`. A field the API does not take is a
105
+ `TypeError` before anything is sent. `create_and_wait` is on `renders`, `edits`, `enhancements` and
106
+ `videos`; a render returns a list with one job per `count`, the others one job.
107
+
108
+ ### Engines, modes and settings
109
+
110
+ `engine`, `mode`, `aspect_ratio`, `camera_movement`, `motion` and the video `model` are typed with
111
+ the values the API takes. `engines.list()` is the live list, with each engine's price and what it
112
+ can do:
113
+
114
+ ```python
115
+ for engine in mnml.engines.list()["engines"]:
116
+ print(engine["id"], engine["prices"]["render"], engine["capabilities"]["max_references"])
117
+ ```
118
+
119
+ ### Images
120
+
121
+ Send the image in the call, as `image`:
122
+
123
+ ```python
124
+ from pathlib import Path
125
+
126
+ mnml.renders.create(image="https://example.com/plan.png", prompt="Brick and glass, dusk")
127
+ mnml.renders.create(image=Path("plan.png"), prompt="Brick and glass, dusk")
128
+ mnml.renders.create(image=png_bytes, prompt="Brick and glass, dusk")
129
+ ```
130
+
131
+ A `str` is sent as it is: a public `https://` link, a `data:image/...;base64,` URI or bare base64.
132
+ Bytes, a `Path` or an open binary file is read and sent as a data URI, up to 15 MB per image. A
133
+ `str` is never read as a file, so pass `Path("plan.png")`, not `"plan.png"`. `image_url` is the
134
+ old name of `image` and still works.
135
+
136
+ Every place an image goes takes the same: `image`, each of `references`, an edit's `mask` and a
137
+ video's `end_frame`. A reference can also be a dict with a `mode`:
138
+
139
+ ```python
140
+ mnml.renders.create(
141
+ image=Path("massing.png"),
142
+ references=[Path("timber.jpg"), {"image": "https://example.com/brick.png", "mode": "material"}],
143
+ prompt="Timber and brick, overcast",
144
+ )
145
+ ```
146
+
147
+ To work on a finished job's output without downloading it, pass its `job_id`:
148
+
149
+ ```python
150
+ mnml.edits.create(job_id=job["id"], prompt="Dark brick instead of render")
151
+ ```
152
+
153
+ ### Edits, enhancements and video
154
+
155
+ ```python
156
+ # Change one thing, inside a box (fractions of the image from its top-left)
157
+ mnml.edits.create(
158
+ job_id=job_id, prompt="A red front door", region={"box": {"x": 0.4, "y": 0.5, "width": 0.2, "height": 0.4}}
159
+ )
160
+
161
+ # Or paint a mask: a PNG, white to change and black to keep
162
+ mnml.edits.create(image=Path("house.jpg"), mask=Path("door-mask.png"), prompt="A red front door")
163
+
164
+ # Remove what a region covers
165
+ mnml.edits.create(job_id=job_id, kind="erase", region={"box": {"x": 0.1, "y": 0.6, "width": 0.2, "height": 0.3}})
166
+
167
+ # Upscale, enhance, cut out, or extend to a new frame
168
+ mnml.enhancements.create(job_id=job_id, kind="outpaint", aspect_ratio="16:9")
169
+
170
+ # A ten-second camera move
171
+ mnml.videos.create(job_id=job_id, model="v2.0-flash", duration_seconds=10, camera_movement="orbit-right")
172
+ ```
173
+
174
+ ### Waiting for a job
175
+
176
+ A render can come back finished from the create call itself: pass `wait` (1-90 seconds) and the
177
+ answer's `jobs` carry the outputs. A read takes `wait` too, and answers as soon as the job settles.
178
+ `create_and_wait` and `jobs.wait` use both, so a render usually takes one or two calls, not a
179
+ polling loop.
180
+
181
+ ```python
182
+ started = mnml.renders.create(image=Path("massing.png"), prompt="Brick, dusk", wait=90)
183
+ print(started.get("jobs", [{}])[0].get("outputs")) # set when it finished in time
184
+
185
+ read = mnml.jobs.get(started["id"], wait=90) # held until it settles
186
+
187
+ job = mnml.jobs.wait(job_id, interval=3.0, timeout=600.0)
188
+ ```
189
+
190
+ `jobs.wait` raises `MnmlTimeoutError` when its own time runs out. The job keeps running, so
191
+ read it again later. `videos.create_and_wait` reads every 10 seconds for up to 20 minutes unless
192
+ you say otherwise. For long jobs such as video, a [webhook](#webhooks) beats polling.
193
+
194
+ ### Following a job as it runs
195
+
196
+ `jobs.stream` follows one job over a single connection (Server-Sent Events) for up to ten
197
+ minutes: each change of the job, then one last event, after which the loop ends. It suits a long
198
+ job in a script with no server to take a webhook.
199
+
200
+ ```python
201
+ for event in mnml.jobs.stream(job_id):
202
+ if event["type"] == "job":
203
+ print(event["job"]["status"]) # as it changes
204
+ elif event["type"] == "done":
205
+ print(event["job"]["outputs"][0]["url"]) # settled
206
+ elif event["type"] == "timeout":
207
+ break # still running after ten minutes: stream it again
208
+ else:
209
+ print(event["error"]["message"]) # the API could not read it; the job still runs
210
+ ```
211
+
212
+ Leaving the loop hangs up; the job keeps running. A connection that drops before the last event
213
+ raises `MnmlError` with `STREAM_ENDED`.
214
+
215
+ ### Downloading outputs
216
+
217
+ Output links are signed and expire after an hour or two. Download what you want to keep:
218
+
219
+ ```python
220
+ file = mnml.files.download(job["outputs"][0], to="render.jpg")
221
+ print(file["content_type"], len(file["data"]))
222
+ ```
223
+
224
+ The link's signature is its credential, so your key is not sent with it. An expired link raises
225
+ `MnmlError` with `NOT_FOUND`; read the job again for fresh links.
226
+
227
+ ## Errors
228
+
229
+ A refusal raises `MnmlError`, carrying the API's `code`, the HTTP `status` and the
230
+ `request_id` to quote to support:
231
+
232
+ ```python
233
+ from mnml_ai import MnmlError
234
+
235
+ try:
236
+ mnml.renders.create(image=url, prompt=prompt)
237
+ except MnmlError as err:
238
+ if err.code == "INSUFFICIENT_CREDITS":
239
+ ... # top up, then send it again
240
+ raise
241
+ ```
242
+
243
+ On `VALIDATION_FAILED`, `err.issues` lists each refused field. Every code is described at
244
+ [developers.mnml.ai/docs/errors](https://developers.mnml.ai/docs/errors).
245
+
246
+ ## Retries and idempotency
247
+
248
+ The client retries a `429`, a `5xx` or a dropped connection, twice by default. It waits out
249
+ `Retry-After` when that is a minute or less, and otherwise raises the error to you. That
250
+ covers a daily limit, which resets at midnight UTC.
251
+
252
+ Every call that can spend credits carries an `Idempotency-Key`, and the client reuses the same
253
+ key across its own retries, so a retry never charges twice. To make your own retries safe as
254
+ well, pass a key you choose:
255
+
256
+ ```python
257
+ mnml.renders.create(prompt=prompt, image=url, idempotency_key=f"order-{order_id}")
258
+ ```
259
+
260
+ ## Webhooks
261
+
262
+ Add an endpoint and copy its signing secret on the
263
+ [Webhooks](https://developers.mnml.ai/console/webhooks) page. Then verify each delivery with
264
+ the raw request body, before parsing it:
265
+
266
+ ```python
267
+ import os
268
+ from flask import Flask, request
269
+ from mnml_ai import WebhookVerificationError, verify_webhook
270
+
271
+ app = Flask(__name__)
272
+
273
+
274
+ @app.post("/webhooks/mnml")
275
+ def mnml_webhook():
276
+ try:
277
+ event = verify_webhook(request.get_data(), request.headers, os.environ["MNML_WEBHOOK_SECRET"])
278
+ except WebhookVerificationError:
279
+ return "", 400
280
+ if event["type"] == "job.succeeded":
281
+ job = event["data"] # the job, as jobs.get returns it
282
+ elif event["type"] == "credits.low":
283
+ balance = event["data"]["balance"] # and event["data"]["threshold"]
284
+ return "", 204
285
+ ```
286
+
287
+ `verify_webhook` raises `WebhookVerificationError` on a bad signature or a stale timestamp
288
+ (over five minutes). Events: `job.succeeded`, `job.failed`, `job.canceled`, `credits.low` and
289
+ `webhook.test`; checking `event["type"]` narrows `event["data"]` for a type checker. The scheme is
290
+ [Standard Webhooks](https://www.standardwebhooks.com). See
291
+ [`examples/`](./examples) for FastAPI and Django.
292
+
293
+ ## Typing
294
+
295
+ The answers are typed as `TypedDict`s in `mnml_ai.types` (`Job`, `RenderStarted`, `JobStreamEvent`,
296
+ `Account`, `Engines`, `WebhookEvent` and more), the values the API takes as `Literal`s (`Mode`,
297
+ `EngineId`, `CameraMove` …), and the package ships `py.typed`. A test in this repository checks
298
+ every keyword argument, field and listed value against the API's OpenAPI document
299
+ ([`spec/openapi.json`](./spec/openapi.json)).
300
+
301
+ ## Moving from the mnmlai.dev API
302
+
303
+ The mnmlai.dev routes (`/v1/archDiffusion-v46`, `/v1/upscale`, `/v1/status/{id}` …) still answer
304
+ at `/v1` on `api.mnml.ai` and on `api.mnmlai.dev`, deprecated, so nothing breaks while you move.
305
+ The SDK speaks API v2 only. Each old route has a v2 call that does the same job:
306
+
307
+ | mnmlai.dev route | SDK call |
308
+ | ------------------------------------------------------------- | ------------------------------------------------------------- |
309
+ | `archDiffusion-v46` | `renders.create(engine="v4.6-ultra", ...)` |
310
+ | `archDiffusion-v45`, `-v45-lite` | `renders.create(engine="v4.5-ultra")`, `"v4.5-fast"` |
311
+ | `archDiffusion-v44`, `-v44-lite` | `renders.create(engine="v4.4-ultra")`, `"v4.4-fast"` |
312
+ | `archDiffusion-v43`, `-v43-lite`, `-v42`, `-v42-lite`, `-v41` | `renders.create(engine="v4.3")`, `"v4.3-fast"` |
313
+ | `mixture-of-experts` | `renders.create(mode=..., ...)` (`expert_name` is the `mode`) |
314
+ | `exterior`, `interior`, `sketch-to-img` | `renders.create(engine="v3.1", mode=..., ...)` |
315
+ | `style/transfer` | `renders.create(references=[{..., "mode": "style"}], ...)` |
316
+ | `imagine-ai` | `renders.create(mode="text-to-render", prompt=...)` |
317
+ | `virtual-staging-ai` (v1 and v2) | `renders.create(mode="interior", ...)` |
318
+ | `inpaint` | `edits.create(prompt=..., mask=...)` or `region` |
319
+ | `ai-eraser` | `edits.create(kind="erase", region=...)` |
320
+ | `upscale`, `render/enhancer` | `enhancements.create(kind="upscale")`, `kind="enhance"` |
321
+ | `video-v20-flash`, `video-v20-cinematic`, `video-ai` | `videos.create(model="v2.0-flash")`, `"v2.0"`, `"v1.1"` |
322
+ | `status/{id}` (v1 and v2) | `jobs.get(id)` or `jobs.wait(id)` |
323
+ | `credits` | `account.get()` |
324
+
325
+ v2 takes the image in the request, as mnmlai.dev did: pass it as `image` (a link, bytes or a
326
+ `Path`), or a `job_id` for a finished job's output. The full guide is at
327
+ [developers.mnml.ai/docs/migrate](https://developers.mnml.ai/docs/migrate).
328
+
329
+ ## Links
330
+
331
+ - [Documentation](https://developers.mnml.ai/docs)
332
+ - [API reference](https://developers.mnml.ai/docs/renders)
333
+ - [TypeScript SDK](https://github.com/mnml-ai/mnml-sdk-typescript)
334
+ - [Changelog](./CHANGELOG.md)
335
+
336
+ ## License
337
+
338
+ [MIT](./LICENSE)