diffusr 0.1.0__py3-none-any.whl

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.
diffusr/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """The diffusr API client. Stdlib only.
2
+
3
+ from diffusr import Diffusr
4
+ d = Diffusr() # reads DIFFUSR_KEY
5
+ quote = d.make_video(prompt="...", dry_run=True) # free
6
+ """
7
+
8
+ from ._client import DEFAULT_BASE_URL, TERMINAL_STATUSES, Diffusr
9
+ from ._errors import DiffusrError
10
+ from ._verify import SIGNATURE_TOLERANCE_SEC, verify_callback
11
+
12
+ __all__ = [
13
+ "Diffusr",
14
+ "DiffusrError",
15
+ "verify_callback",
16
+ "DEFAULT_BASE_URL",
17
+ "TERMINAL_STATUSES",
18
+ "SIGNATURE_TOLERANCE_SEC",
19
+ ]
20
+ __version__ = "0.1.0"
diffusr/_client.py ADDED
@@ -0,0 +1,287 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import mimetypes
5
+ import os
6
+ import socket
7
+ import time
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+ import uuid
12
+ from typing import Any, Callable, Dict, Mapping, Optional, Union
13
+
14
+ from ._errors import DiffusrError
15
+ from ._verify import verify_callback as _verify_callback
16
+
17
+ DEFAULT_BASE_URL = "https://app.diffusr.ai/api/v2"
18
+ SDK_VERSION = "0.1.0"
19
+ TERMINAL_STATUSES = ("SUCCEEDED", "FAILED", "CANCELED")
20
+ # Never sleep longer than this on one automatic retry; raise instead.
21
+ MAX_AUTO_RETRY_WAIT_SEC = 30
22
+
23
+ _CONTENT_TYPES = {
24
+ ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp",
25
+ ".gif": "image/gif", ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm",
26
+ ".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4",
27
+ }
28
+
29
+
30
+ class Diffusr:
31
+ """Client for the diffusr REST API (v2). Bodies and answers are the API's own snake_case JSON.
32
+
33
+ api_key dfsr_live_... or dfsr_test_... (free). Default: the DIFFUSR_KEY environment variable.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ api_key: Optional[str] = None,
39
+ base_url: Optional[str] = None,
40
+ *,
41
+ max_retries: int = 3,
42
+ retry_base: float = 0.5,
43
+ timeout: float = 60.0,
44
+ ) -> None:
45
+ key = api_key or os.environ.get("DIFFUSR_KEY")
46
+ if not key:
47
+ raise DiffusrError(0, "MISSING_API_KEY", "Pass an API key or set DIFFUSR_KEY.")
48
+ self._key = key
49
+ self._base = (base_url or DEFAULT_BASE_URL).rstrip("/")
50
+ self._max_retries = max_retries
51
+ self._retry_base = retry_base
52
+ self._timeout = timeout
53
+
54
+ # ——— make calls (paid; dry_run=True is free) ———
55
+
56
+ def make_video(self, **body: Any) -> Dict[str, Any]:
57
+ return self._make("make_video", body)
58
+
59
+ def make_image(self, **body: Any) -> Dict[str, Any]:
60
+ return self._make("make_image", body)
61
+
62
+ def edit_image(self, **body: Any) -> Dict[str, Any]:
63
+ return self._make("edit_image", body)
64
+
65
+ def recast(self, **body: Any) -> Dict[str, Any]:
66
+ return self._make("recast", body)
67
+
68
+ def make_music(self, **body: Any) -> Dict[str, Any]:
69
+ return self._make("make_music", body)
70
+
71
+ def creator_video(self, **body: Any) -> Dict[str, Any]:
72
+ return self._make("creator_video", body)
73
+
74
+ def run_template(self, **body: Any) -> Dict[str, Any]:
75
+ """Run one of diffusr's templates. One flat body: template="<id from list_templates()>",
76
+ plus the fields that template takes (mode, prompt, pictures, image_urls, product_image_url,
77
+ script, place, people, setup, resolution, dry_run, callback_url)."""
78
+ return self._make("run_template", body)
79
+
80
+ def _make(self, tool: str, body: Dict[str, Any]) -> Dict[str, Any]:
81
+ # One Idempotency-Key per call, reused by every automatic retry.
82
+ key = body.pop("idempotency_key", None) or str(uuid.uuid4())
83
+ return self._request("POST", f"/{tool}", body=body, headers={"Idempotency-Key": key})
84
+
85
+ # ——— results ———
86
+
87
+ def get_result(self, id: str) -> Dict[str, Any]:
88
+ return self._request("GET", f"/results/{urllib.parse.quote(id, safe='')}")
89
+
90
+ def list_results(self, limit: Optional[int] = None) -> Dict[str, Any]:
91
+ return self._request("GET", "/results" + _query(limit=limit))
92
+
93
+ def cancel(self, id: str) -> Dict[str, Any]:
94
+ """Stop queued work before it generates (that part is never charged).
95
+
96
+ A step that is already generating may still finish and be charged: check
97
+ result["charge"] on the answer. A finished job comes back unchanged.
98
+ """
99
+ return self._request("POST", f"/results/{urllib.parse.quote(id, safe='')}/cancel", body={})
100
+
101
+ def wait_for(
102
+ self,
103
+ id: str,
104
+ timeout: float = 900.0,
105
+ interval: float = 3.0,
106
+ on_poll: Optional[Callable[[Dict[str, Any]], None]] = None,
107
+ ) -> Dict[str, Any]:
108
+ """Poll until the job is over and return the result (seconds, not ms).
109
+
110
+ A job that ends FAILED or CANCELED is returned too: check result["status"].
111
+ Raises DiffusrError(code="WAIT_TIMEOUT") if it is still running at the
112
+ deadline (the job itself keeps going).
113
+ """
114
+ deadline = time.monotonic() + timeout
115
+ pause = interval
116
+ while True:
117
+ result = self.get_result(id)
118
+ if on_poll:
119
+ on_poll(result)
120
+ if result.get("status") in TERMINAL_STATUSES:
121
+ return result
122
+ left = deadline - time.monotonic()
123
+ if left <= 0:
124
+ break
125
+ time.sleep(min(pause, left))
126
+ pause = min(pause * 1.5, max(15.0, interval))
127
+ if time.monotonic() >= deadline:
128
+ break
129
+ raise DiffusrError(
130
+ 0,
131
+ "WAIT_TIMEOUT",
132
+ f"Result {id} was still not finished after {int(timeout)} s. It keeps running: call wait_for or get_result again.",
133
+ )
134
+
135
+ # ——— files ———
136
+
137
+ def upload_file(
138
+ self, file: Union[str, "os.PathLike[str]", bytes, bytearray], content_type: Optional[str] = None
139
+ ) -> Dict[str, Any]:
140
+ """Upload a local file (a path, or bytes) and get a file_url to pass to a tool.
141
+
142
+ Two steps: ask for a signed URL, PUT the exact bytes to it.
143
+ """
144
+ if isinstance(file, (bytes, bytearray)):
145
+ data = bytes(file)
146
+ else:
147
+ path = os.fspath(file)
148
+ with open(path, "rb") as f:
149
+ data = f.read()
150
+ content_type = content_type or _CONTENT_TYPES.get(os.path.splitext(path)[1].lower()) or mimetypes.guess_type(path)[0]
151
+ if not content_type:
152
+ raise DiffusrError(0, "CONTENT_TYPE_REQUIRED", 'Pass content_type (e.g. "video/mp4"): it could not be worked out.')
153
+ ticket = self._request("POST", "/uploads", body={"content_type": content_type, "size_bytes": len(data)})
154
+ req = urllib.request.Request(ticket["upload_url"], data=data, method=ticket.get("method", "PUT"))
155
+ for k, v in ticket.get("headers", {}).items():
156
+ req.add_header(k, v)
157
+ try:
158
+ with urllib.request.urlopen(req, timeout=self._timeout):
159
+ pass
160
+ except urllib.error.HTTPError as e:
161
+ raise DiffusrError(e.code, "UPLOAD_FAILED", f"The upload PUT failed with HTTP {e.code}.") from None
162
+ except urllib.error.URLError as e:
163
+ raise DiffusrError(0, "NETWORK_ERROR", f"The upload PUT failed: {e.reason}") from None
164
+ return {"file_url": ticket["file_url"], "kind": ticket.get("kind")}
165
+
166
+ def upload_from_url(self, source_url: str) -> Dict[str, Any]:
167
+ """Have us copy a public link in (images and short clips; use upload_file for big videos)."""
168
+ return self._request("POST", "/uploads", body={"source_url": source_url})
169
+
170
+ # ——— catalog ———
171
+
172
+ def list_models(self, kind: Optional[str] = None) -> Dict[str, Any]:
173
+ return self._request("GET", "/models" + _query(kind=kind))
174
+
175
+ def list_templates(self) -> Dict[str, Any]:
176
+ return self._request("GET", "/templates")
177
+
178
+ # ——— callbacks ———
179
+
180
+ def list_callbacks(self, limit: Optional[int] = None, cursor: Optional[str] = None) -> Dict[str, Any]:
181
+ return self._request("GET", "/callbacks" + _query(limit=limit, cursor=cursor))
182
+
183
+ def retry_callback(self, event_id: str) -> Dict[str, Any]:
184
+ """One more try for a FAILED event (up to 3 by hand)."""
185
+ return self._request("POST", f"/callbacks/{urllib.parse.quote(event_id, safe='')}/retry", body={})
186
+
187
+ def get_callback_secret(self) -> Dict[str, Any]:
188
+ """Your signing secret (whsec_...) for Diffusr.verify_callback."""
189
+ return self._request("GET", "/callbacks/secret")
190
+
191
+ def rotate_callback_secret(self) -> Dict[str, Any]:
192
+ return self._request("POST", "/callbacks/secret/rotate", body={})
193
+
194
+ @staticmethod
195
+ def verify_callback(
196
+ raw_body: Union[str, bytes],
197
+ headers: Mapping[str, str],
198
+ secret: str,
199
+ tolerance_sec: int = 300,
200
+ now: Optional[float] = None,
201
+ ) -> bool:
202
+ """Check a callback's signature (Standard Webhooks). Rejects a timestamp more than 5 minutes off."""
203
+ return _verify_callback(raw_body, headers, secret, tolerance_sec, now)
204
+
205
+ # ——— plumbing ———
206
+
207
+ def _backoff(self, attempt: int) -> float:
208
+ return self._retry_base * (2**attempt)
209
+
210
+ def _request(
211
+ self, method: str, path: str, body: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None
212
+ ) -> Dict[str, Any]:
213
+ hdrs = {
214
+ "Authorization": f"Bearer {self._key}",
215
+ "Accept": "application/json",
216
+ "User-Agent": f"diffusr-python/{SDK_VERSION}",
217
+ }
218
+ data = None
219
+ if body is not None:
220
+ hdrs["Content-Type"] = "application/json"
221
+ data = json.dumps(body).encode("utf-8")
222
+ hdrs.update(headers or {})
223
+
224
+ attempt = 0
225
+ while True:
226
+ req = urllib.request.Request(self._base + path, data=data, method=method, headers=hdrs)
227
+ try:
228
+ with urllib.request.urlopen(req, timeout=self._timeout) as res:
229
+ text = res.read().decode("utf-8")
230
+ return json.loads(text) if text else {}
231
+ except urllib.error.HTTPError as e:
232
+ err = _to_error(e)
233
+ wait = err.retry_after if err.retry_after is not None else self._backoff(attempt)
234
+ retryable = e.code in (429, 503) or (method == "GET" and e.code in (500, 502, 504))
235
+ if retryable and attempt < self._max_retries and wait <= MAX_AUTO_RETRY_WAIT_SEC:
236
+ time.sleep(wait)
237
+ attempt += 1
238
+ continue
239
+ raise err from None
240
+ except (urllib.error.URLError, OSError) as e:
241
+ # OSError covers socket.timeout (a separate class before 3.10), TimeoutError, ConnectionError.
242
+ if attempt < self._max_retries:
243
+ time.sleep(self._backoff(attempt))
244
+ attempt += 1
245
+ continue
246
+ reason = getattr(e, "reason", e)
247
+ if _is_timeout(e) or _is_timeout(reason):
248
+ raise DiffusrError(0, "TIMEOUT", f"diffusr did not answer within {self._timeout:g} s.") from None
249
+ raise DiffusrError(0, "NETWORK_ERROR", f"Could not reach diffusr: {reason}") from None
250
+
251
+
252
+ def _is_timeout(e: Any) -> bool:
253
+ return isinstance(e, (socket.timeout, TimeoutError))
254
+
255
+
256
+ def _query(**params: Any) -> str:
257
+ q = urllib.parse.urlencode({k: v for k, v in params.items() if v is not None})
258
+ return f"?{q}" if q else ""
259
+
260
+
261
+ def _to_error(e: urllib.error.HTTPError) -> DiffusrError:
262
+ text = ""
263
+ try:
264
+ text = e.read().decode("utf-8", "replace")
265
+ except Exception:
266
+ pass
267
+ body: Optional[Dict[str, Any]] = None
268
+ try:
269
+ parsed = json.loads(text)
270
+ if isinstance(parsed, dict):
271
+ body = parsed
272
+ except ValueError:
273
+ pass
274
+ retry_after: Optional[float] = None
275
+ header = e.headers.get("Retry-After") if e.headers else None
276
+ if header is not None:
277
+ try:
278
+ retry_after = float(header)
279
+ except ValueError:
280
+ pass
281
+ if retry_after is None and body and isinstance(body.get("retry_after_seconds"), (int, float)):
282
+ retry_after = float(body["retry_after_seconds"])
283
+ message = body["error"] if body and isinstance(body.get("error"), str) else (text[:300] or f"HTTP {e.code}")
284
+ code = body["code"] if body and isinstance(body.get("code"), str) else f"HTTP_{e.code}"
285
+ request_id = body.get("request_id") if body and isinstance(body.get("request_id"), str) else None
286
+ request_id = request_id or (e.headers.get("X-Request-Id") if e.headers else None)
287
+ return DiffusrError(e.code, code, message, request_id, retry_after, body)
diffusr/_errors.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+
6
+ class DiffusrError(Exception):
7
+ """Every non-2xx answer (and a few client-side failures) raises this.
8
+
9
+ status HTTP status; 0 when no answer came (network error, wait timeout)
10
+ code the API's machine code, e.g. "RATE_LIMITED"
11
+ message the API's plain-words error
12
+ request_id quote it to support
13
+ retry_after seconds to wait before trying again, when the API said so
14
+ body the whole JSON error body (field, issues, budget...)
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ status: int,
20
+ code: str,
21
+ message: str,
22
+ request_id: Optional[str] = None,
23
+ retry_after: Optional[float] = None,
24
+ body: Optional[Dict[str, Any]] = None,
25
+ ) -> None:
26
+ super().__init__(message)
27
+ self.status = status
28
+ self.code = code
29
+ self.message = message
30
+ self.request_id = request_id
31
+ self.retry_after = retry_after
32
+ self.body = body
33
+
34
+ def __str__(self) -> str:
35
+ rid = f" (request_id {self.request_id})" if self.request_id else ""
36
+ return f"[{self.code}] {self.message}{rid}"
diffusr/_verify.py ADDED
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import binascii
5
+ import hashlib
6
+ import hmac
7
+ import time
8
+ from typing import Mapping, Optional, Union
9
+
10
+ SIGNATURE_TOLERANCE_SEC = 300
11
+
12
+
13
+ def _header(headers: Mapping[str, str], name: str) -> Optional[str]:
14
+ for k, v in headers.items():
15
+ if k.lower() == name:
16
+ return v if isinstance(v, str) else " ".join(v)
17
+ return None
18
+
19
+
20
+ def verify_callback(
21
+ raw_body: Union[str, bytes],
22
+ headers: Mapping[str, str],
23
+ secret: str,
24
+ tolerance_sec: int = SIGNATURE_TOLERANCE_SEC,
25
+ now: Optional[float] = None,
26
+ ) -> bool:
27
+ """Check a diffusr callback (Standard Webhooks).
28
+
29
+ raw_body is the request body exactly as received, before JSON parsing.
30
+ Returns False for a missing header, a bad or stale signature; never raises.
31
+ """
32
+ try:
33
+ msg_id = _header(headers, "webhook-id")
34
+ timestamp = _header(headers, "webhook-timestamp")
35
+ signature = _header(headers, "webhook-signature")
36
+ if not msg_id or not timestamp or not signature or not timestamp.isdigit():
37
+ return False
38
+ current = time.time() if now is None else now
39
+ if abs(int(current) - int(timestamp)) > tolerance_sec:
40
+ return False
41
+ body = raw_body if isinstance(raw_body, (bytes, bytearray)) else raw_body.encode("utf-8")
42
+ key = base64.b64decode(secret[6:] if secret.startswith("whsec_") else secret)
43
+ expected = hmac.new(key, f"{msg_id}.{timestamp}.".encode("utf-8") + bytes(body), hashlib.sha256).digest()
44
+ ok = False
45
+ for part in signature.split(" "):
46
+ if not part.startswith("v1,"):
47
+ continue
48
+ try:
49
+ given = base64.b64decode(part[3:], validate=True)
50
+ except (binascii.Error, ValueError):
51
+ continue
52
+ if hmac.compare_digest(given, expected):
53
+ ok = True
54
+ return ok
55
+ except Exception:
56
+ return False
diffusr/py.typed ADDED
File without changes
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.5
2
+ Name: diffusr
3
+ Version: 0.1.0
4
+ Summary: The diffusr API client: make videos, images and music from code. Free price checks, idempotent starts, wait_for, signed callbacks.
5
+ Project-URL: Homepage, https://diffusr.ai
6
+ Project-URL: Documentation, https://diffusr.ai/docs
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: ai,api,diffusr,image,video
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+
16
+ # diffusr
17
+
18
+ The diffusr API client for Python 3.9+. Make videos, images and music from
19
+ code. Standard library only. Bodies and answers are the API's own snake_case
20
+ JSON (plain dicts).
21
+
22
+ ```sh
23
+ pip install diffusr
24
+ ```
25
+
26
+ ## Quickstart
27
+
28
+ Use a **test key** (`dfsr_test_…`) while you build: prices are quoted exactly,
29
+ nothing is charged, no model runs, and the result is a placeholder file a few
30
+ seconds later. Swap in a live key (`dfsr_live_…`) when you are ready to spend.
31
+ Mint keys on the Developers page in the app.
32
+
33
+ ```python
34
+ from diffusr import Diffusr
35
+
36
+ diffusr = Diffusr() # reads DIFFUSR_KEY
37
+
38
+ # 1. Ask the price. Free: nothing is made, nothing is spent.
39
+ quote = diffusr.make_video(
40
+ prompt="A can of sparkling water on a wet street, slow push-in, rain",
41
+ duration_sec=5,
42
+ dry_run=True,
43
+ )
44
+ print(quote["price"]) # {'credits': 63, 'usd': '0.6300', ...}
45
+
46
+ # 2. Make it. An Idempotency-Key is added for you and reused on retries,
47
+ # so a dropped connection never pays twice.
48
+ job = diffusr.make_video(
49
+ prompt="A can of sparkling water on a wet street, slow push-in, rain",
50
+ duration_sec=5,
51
+ )
52
+
53
+ # 3. Wait for it (polls with backoff; raises WAIT_TIMEOUT if it takes too long).
54
+ result = diffusr.wait_for(job["id"], timeout=600)
55
+ if result["status"] == "SUCCEEDED":
56
+ print(result["output"]["url"])
57
+ else:
58
+ print(result["status"], result.get("error"))
59
+ print(result["charge"]) # quoted_credits, charged_credits, final
60
+ ```
61
+
62
+ ### Callbacks instead of polling
63
+
64
+ Pass `callback_url` (https, public) and we POST one signed event when the job
65
+ ends. Verify it with your signing secret (`diffusr.get_callback_secret()["secret"]`):
66
+
67
+ ```python
68
+ from diffusr import Diffusr
69
+
70
+ # Flask: use the RAW body, not the parsed one.
71
+ @app.post("/diffusr")
72
+ def hook():
73
+ raw = request.get_data(as_text=True)
74
+ if not Diffusr.verify_callback(raw, request.headers, os.environ["DIFFUSR_WHSEC"]):
75
+ return "", 400
76
+ event = json.loads(raw) # {id, type: "result.finished", data: <result>}
77
+ return "", 200 # any 2xx; drop repeats of event["id"]
78
+ ```
79
+
80
+ `verify_callback` implements [Standard Webhooks](https://www.standardwebhooks.com):
81
+ constant-time compare, two signatures after a secret rotation, a 5-minute
82
+ timestamp window, `whsec_` prefix handled.
83
+
84
+ ## Methods
85
+
86
+ | Method | REST |
87
+ |---|---|
88
+ | `make_video`, `make_image`, `edit_image`, `recast`, `make_music`, `creator_video`, `run_template` `(**body)` | `POST /{tool}` |
89
+ | `get_result(id)`, `list_results(limit=)`, `cancel(id)` (stops queued work; a step already generating may finish and be charged: check `result["charge"]`) | `GET /results/{id}`, `GET /results`, `POST /results/{id}/cancel` |
90
+ | `wait_for(id, timeout=, interval=, on_poll=)` | polls `GET /results/{id}` |
91
+ | `upload_file(path_or_bytes, content_type=)`, `upload_from_url(url)` | `POST /uploads` + signed `PUT` |
92
+ | `list_models(kind=)`, `list_templates()` | `GET /models`, `GET /templates` |
93
+ | `list_callbacks(limit=, cursor=)`, `retry_callback(event_id)`, `get_callback_secret()`, `rotate_callback_secret()` | `/callbacks…` |
94
+ | `Diffusr.verify_callback(raw_body, headers, secret)` | |
95
+
96
+ `run_template` takes one flat body: `template="<id from list_templates()>"` plus the fields that
97
+ template takes (`mode`, `prompt`, `pictures`, `image_urls`, `product_image_url`, `script`, `place`,
98
+ `people`, `setup`, `resolution`):
99
+
100
+ ```python
101
+ diffusr.run_template(template="tpl-ugc-talking-head", script="Hi!", people=[{"name": "Mia", "image_url": "https://…"}], dry_run=True)
102
+ ```
103
+
104
+ `dry_run=True` on any make call answers with the price only. Pass
105
+ `idempotency_key="…"` to use your own key.
106
+
107
+ ## Retries and errors
108
+
109
+ - Every request times out after 60 s (`Diffusr(timeout=...)`) with a `TIMEOUT` error.
110
+ - `GET`s (and so `wait_for`) retry on 429, 500, 502, 503, 504, network errors
111
+ and timeouts with backoff, honouring `Retry-After` (waits over 30 s are not
112
+ slept: the error is raised).
113
+ - Make calls retry on 429, 503, network errors and timeouts only, **with the
114
+ same Idempotency-Key**.
115
+ - Every failure is a `DiffusrError`:
116
+
117
+ ```python
118
+ from diffusr import DiffusrError
119
+
120
+ try:
121
+ diffusr.make_video(prompt="…")
122
+ except DiffusrError as e:
123
+ print(e.status, e.code, e.message, e.request_id, e.retry_after)
124
+ # e.body has the extras: issues, field, budget…
125
+ ```
126
+
127
+ Codes are the API's (`RATE_LIMITED`, `BUDGET_EXCEEDED`, `INSUFFICIENT_CREDITS`…).
128
+ The SDK adds `NETWORK_ERROR`, `TIMEOUT`, `WAIT_TIMEOUT`, `UPLOAD_FAILED`,
129
+ `CONTENT_TYPE_REQUIRED` and `MISSING_API_KEY`. Handle codes you don't know.
130
+
131
+ ## Docs
132
+
133
+ Full API reference, prices and agent guides: https://diffusr.ai/docs
134
+
135
+ MIT licensed.
@@ -0,0 +1,9 @@
1
+ diffusr/__init__.py,sha256=GJu0H3cqXBbB8VunnZqrzq0rqAGhOGSlEpP0i1Fasvo,516
2
+ diffusr/_client.py,sha256=MMyVDdmff1nIQSuiFKf_9c0sJDITUSY-fkNQjodgKyI,12005
3
+ diffusr/_errors.py,sha256=YWrBDO9lzEqHgckTc66uLcpETfvbWmeyhmB2rFVJkzc,1184
4
+ diffusr/_verify.py,sha256=d5pNG-qf3CtXWuI5Ejm614kB96o0shMPE9xa34s_ZgM,1954
5
+ diffusr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ diffusr-0.1.0.dist-info/METADATA,sha256=XzZOenx6fc7hiOEjMPj-vlsA6LzXjNtfus3xZDAC76A,5151
7
+ diffusr-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
8
+ diffusr-0.1.0.dist-info/licenses/LICENSE,sha256=1l1mtdgkeKxmibt4kNGlmLRfZhUQWNx-rJB0lQD_dWY,1064
9
+ diffusr-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 diffusr
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.