delula-sdk 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.
- delula_sdk-0.1.0/PKG-INFO +59 -0
- delula_sdk-0.1.0/README.md +38 -0
- delula_sdk-0.1.0/delula_sdk/__init__.py +6 -0
- delula_sdk-0.1.0/delula_sdk/client.py +122 -0
- delula_sdk-0.1.0/delula_sdk/errors.py +17 -0
- delula_sdk-0.1.0/delula_sdk.egg-info/PKG-INFO +59 -0
- delula_sdk-0.1.0/delula_sdk.egg-info/SOURCES.txt +10 -0
- delula_sdk-0.1.0/delula_sdk.egg-info/dependency_links.txt +1 -0
- delula_sdk-0.1.0/delula_sdk.egg-info/top_level.txt +1 -0
- delula_sdk-0.1.0/pyproject.toml +32 -0
- delula_sdk-0.1.0/setup.cfg +4 -0
- delula_sdk-0.1.0/tests/test_integration.py +120 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: delula-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Delula Public API SDK (beta)
|
|
5
|
+
Author: Delula
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://delu.la
|
|
8
|
+
Project-URL: Repository, https://github.com/tcotten-scrypted/MagicVidCreator
|
|
9
|
+
Project-URL: Issues, https://github.com/tcotten-scrypted/MagicVidCreator/issues
|
|
10
|
+
Keywords: delula,api,generative-ai,video,image
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# delula-sdk
|
|
23
|
+
|
|
24
|
+
Python client for the [Delula Public API](https://delu.la) (beta).
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install delula-sdk
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requires Python **3.10+**. No third-party dependencies (stdlib only).
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
Create an API key in Delula → **Account → API**, then:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import uuid
|
|
40
|
+
from delula_sdk import DelulaClient
|
|
41
|
+
|
|
42
|
+
client = DelulaClient(api_key="dlu_...")
|
|
43
|
+
|
|
44
|
+
credits = client.get_credits()
|
|
45
|
+
recipes = client.list_recipes()["recipes"]
|
|
46
|
+
preflight = client.preflight_generation(recipes[0]["id"], form_data={})
|
|
47
|
+
gen = client.create_generation(
|
|
48
|
+
recipes[0]["id"],
|
|
49
|
+
form_data={},
|
|
50
|
+
idempotency_key=str(uuid.uuid4()),
|
|
51
|
+
)
|
|
52
|
+
result = client.wait_for_generation(gen["generationId"])
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Default base URL: `https://delu.la/public/api`. Pass `base_url=` for local dev.
|
|
56
|
+
|
|
57
|
+
## Beta
|
|
58
|
+
|
|
59
|
+
Breaking changes may ship in `0.x` without a `/v1` path bump. Pin your version in production.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# delula-sdk
|
|
2
|
+
|
|
3
|
+
Python client for the [Delula Public API](https://delu.la) (beta).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install delula-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python **3.10+**. No third-party dependencies (stdlib only).
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
Create an API key in Delula → **Account → API**, then:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import uuid
|
|
19
|
+
from delula_sdk import DelulaClient
|
|
20
|
+
|
|
21
|
+
client = DelulaClient(api_key="dlu_...")
|
|
22
|
+
|
|
23
|
+
credits = client.get_credits()
|
|
24
|
+
recipes = client.list_recipes()["recipes"]
|
|
25
|
+
preflight = client.preflight_generation(recipes[0]["id"], form_data={})
|
|
26
|
+
gen = client.create_generation(
|
|
27
|
+
recipes[0]["id"],
|
|
28
|
+
form_data={},
|
|
29
|
+
idempotency_key=str(uuid.uuid4()),
|
|
30
|
+
)
|
|
31
|
+
result = client.wait_for_generation(gen["generationId"])
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Default base URL: `https://delu.la/public/api`. Pass `base_url=` for local dev.
|
|
35
|
+
|
|
36
|
+
## Beta
|
|
37
|
+
|
|
38
|
+
Breaking changes may ship in `0.x` without a `/v1` path bump. Pin your version in production.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib import error, request
|
|
8
|
+
|
|
9
|
+
from delula_sdk.errors import DelulaApiError
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = "https://delu.la/public/api"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DelulaClient:
|
|
15
|
+
def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL) -> None:
|
|
16
|
+
self.api_key = api_key
|
|
17
|
+
self.base_url = base_url.rstrip("/")
|
|
18
|
+
|
|
19
|
+
def _request(
|
|
20
|
+
self,
|
|
21
|
+
method: str,
|
|
22
|
+
path: str,
|
|
23
|
+
body: dict[str, Any] | None = None,
|
|
24
|
+
extra_headers: dict[str, str] | None = None,
|
|
25
|
+
) -> Any:
|
|
26
|
+
url = f"{self.base_url}{path}"
|
|
27
|
+
headers = {
|
|
28
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
29
|
+
"Accept": "application/json",
|
|
30
|
+
}
|
|
31
|
+
data: bytes | None = None
|
|
32
|
+
if body is not None:
|
|
33
|
+
headers["Content-Type"] = "application/json"
|
|
34
|
+
data = json.dumps(body).encode("utf-8")
|
|
35
|
+
if extra_headers:
|
|
36
|
+
headers.update(extra_headers)
|
|
37
|
+
req = request.Request(url, data=data, headers=headers, method=method)
|
|
38
|
+
try:
|
|
39
|
+
with request.urlopen(req, timeout=120) as res:
|
|
40
|
+
payload = res.read().decode("utf-8")
|
|
41
|
+
return json.loads(payload) if payload else {}
|
|
42
|
+
except error.HTTPError as exc:
|
|
43
|
+
raw = exc.read().decode("utf-8")
|
|
44
|
+
try:
|
|
45
|
+
parsed = json.loads(raw)
|
|
46
|
+
except json.JSONDecodeError:
|
|
47
|
+
parsed = {}
|
|
48
|
+
raise DelulaApiError(
|
|
49
|
+
exc.code,
|
|
50
|
+
parsed.get("code", "internal_error"),
|
|
51
|
+
parsed.get("message", exc.reason),
|
|
52
|
+
parsed.get("details"),
|
|
53
|
+
) from exc
|
|
54
|
+
|
|
55
|
+
def get_account(self) -> dict[str, Any]:
|
|
56
|
+
return self._request("GET", "/account")
|
|
57
|
+
|
|
58
|
+
def get_credits(self) -> dict[str, Any]:
|
|
59
|
+
return self._request("GET", "/account/credits")
|
|
60
|
+
|
|
61
|
+
def list_recipes(self, lang: str = "en") -> dict[str, Any]:
|
|
62
|
+
return self._request("GET", f"/recipes?lang={lang}")
|
|
63
|
+
|
|
64
|
+
def get_recipe(self, recipe_id: int, lang: str = "en") -> dict[str, Any]:
|
|
65
|
+
return self._request("GET", f"/recipes/{recipe_id}?lang={lang}")
|
|
66
|
+
|
|
67
|
+
def preflight_generation(
|
|
68
|
+
self, recipe_id: int, form_data: dict[str, Any] | None = None
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
return self._request(
|
|
71
|
+
"POST",
|
|
72
|
+
"/generations/preflight",
|
|
73
|
+
{"recipeId": recipe_id, "formData": form_data or {}},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def create_generation(
|
|
77
|
+
self,
|
|
78
|
+
recipe_id: int,
|
|
79
|
+
form_data: dict[str, Any] | None = None,
|
|
80
|
+
parameters: dict[str, Any] | None = None,
|
|
81
|
+
idempotency_key: str | None = None,
|
|
82
|
+
) -> dict[str, Any]:
|
|
83
|
+
key = idempotency_key or str(uuid.uuid4())
|
|
84
|
+
return self._request(
|
|
85
|
+
"POST",
|
|
86
|
+
"/generations",
|
|
87
|
+
{
|
|
88
|
+
"recipeId": recipe_id,
|
|
89
|
+
"formData": form_data or {},
|
|
90
|
+
"parameters": parameters or {},
|
|
91
|
+
},
|
|
92
|
+
extra_headers={"Idempotency-Key": key},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def get_generation(self, generation_id: int) -> dict[str, Any]:
|
|
96
|
+
return self._request("GET", f"/generations/{generation_id}")
|
|
97
|
+
|
|
98
|
+
def list_generations(
|
|
99
|
+
self,
|
|
100
|
+
page: int = 1,
|
|
101
|
+
limit: int = 20,
|
|
102
|
+
status: str | None = None,
|
|
103
|
+
) -> dict[str, Any]:
|
|
104
|
+
query = f"page={page}&limit={limit}"
|
|
105
|
+
if status:
|
|
106
|
+
query += f"&status={status}"
|
|
107
|
+
return self._request("GET", f"/generations?{query}")
|
|
108
|
+
|
|
109
|
+
def wait_for_generation(
|
|
110
|
+
self,
|
|
111
|
+
generation_id: int,
|
|
112
|
+
interval_s: float = 3.0,
|
|
113
|
+
timeout_s: float = 600.0,
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
started = time.time()
|
|
116
|
+
while True:
|
|
117
|
+
gen = self.get_generation(generation_id)
|
|
118
|
+
if gen.get("status") in ("completed", "failed"):
|
|
119
|
+
return gen
|
|
120
|
+
if time.time() - started > timeout_s:
|
|
121
|
+
raise DelulaApiError(408, "internal_error", f"Timed out waiting for generation {generation_id}")
|
|
122
|
+
time.sleep(interval_s)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DelulaApiError(Exception):
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
status: int,
|
|
10
|
+
code: str,
|
|
11
|
+
message: str,
|
|
12
|
+
details: dict[str, Any] | None = None,
|
|
13
|
+
) -> None:
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.status = status
|
|
16
|
+
self.code = code
|
|
17
|
+
self.details = details or {}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: delula-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Delula Public API SDK (beta)
|
|
5
|
+
Author: Delula
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://delu.la
|
|
8
|
+
Project-URL: Repository, https://github.com/tcotten-scrypted/MagicVidCreator
|
|
9
|
+
Project-URL: Issues, https://github.com/tcotten-scrypted/MagicVidCreator/issues
|
|
10
|
+
Keywords: delula,api,generative-ai,video,image
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# delula-sdk
|
|
23
|
+
|
|
24
|
+
Python client for the [Delula Public API](https://delu.la) (beta).
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install delula-sdk
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requires Python **3.10+**. No third-party dependencies (stdlib only).
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
Create an API key in Delula → **Account → API**, then:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import uuid
|
|
40
|
+
from delula_sdk import DelulaClient
|
|
41
|
+
|
|
42
|
+
client = DelulaClient(api_key="dlu_...")
|
|
43
|
+
|
|
44
|
+
credits = client.get_credits()
|
|
45
|
+
recipes = client.list_recipes()["recipes"]
|
|
46
|
+
preflight = client.preflight_generation(recipes[0]["id"], form_data={})
|
|
47
|
+
gen = client.create_generation(
|
|
48
|
+
recipes[0]["id"],
|
|
49
|
+
form_data={},
|
|
50
|
+
idempotency_key=str(uuid.uuid4()),
|
|
51
|
+
)
|
|
52
|
+
result = client.wait_for_generation(gen["generationId"])
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Default base URL: `https://delu.la/public/api`. Pass `base_url=` for local dev.
|
|
56
|
+
|
|
57
|
+
## Beta
|
|
58
|
+
|
|
59
|
+
Breaking changes may ship in `0.x` without a `/v1` path bump. Pin your version in production.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
delula_sdk/__init__.py
|
|
4
|
+
delula_sdk/client.py
|
|
5
|
+
delula_sdk/errors.py
|
|
6
|
+
delula_sdk.egg-info/PKG-INFO
|
|
7
|
+
delula_sdk.egg-info/SOURCES.txt
|
|
8
|
+
delula_sdk.egg-info/dependency_links.txt
|
|
9
|
+
delula_sdk.egg-info/top_level.txt
|
|
10
|
+
tests/test_integration.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
delula_sdk
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "delula-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Delula Public API SDK (beta)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Proprietary" }
|
|
12
|
+
authors = [{ name = "Delula" }]
|
|
13
|
+
keywords = ["delula", "api", "generative-ai", "video", "image"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
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
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://delu.la"
|
|
27
|
+
Repository = "https://github.com/tcotten-scrypted/MagicVidCreator"
|
|
28
|
+
Issues = "https://github.com/tcotten-scrypted/MagicVidCreator/issues"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["."]
|
|
32
|
+
include = ["delula_sdk*"]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Live Public API integration tests for delula-sdk."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
import uuid
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# repo root on path for shared helpers + package under test
|
|
12
|
+
ROOT = Path(__file__).resolve().parents[3]
|
|
13
|
+
sys.path.insert(0, str(ROOT / "sdk" / "python"))
|
|
14
|
+
sys.path.insert(0, str(ROOT / "sdk" / "tests"))
|
|
15
|
+
|
|
16
|
+
from delula_sdk import DelulaClient # noqa: E402
|
|
17
|
+
from delula_sdk.errors import DelulaApiError # noqa: E402
|
|
18
|
+
from recipe_fixtures import build_minimal_form_data, pick_cheapest_recipe # noqa: E402
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_config() -> tuple[str, str, bool, int | None]:
|
|
22
|
+
api_key = (os.environ.get("DELULA_API_KEY") or "").strip()
|
|
23
|
+
if not api_key:
|
|
24
|
+
raise unittest.SkipTest("DELULA_API_KEY is required for SDK integration tests")
|
|
25
|
+
base_url = (os.environ.get("DELULA_API_BASE_URL") or "https://delu.la/public/api").rstrip("/")
|
|
26
|
+
run_write = os.environ.get("DELULA_SDK_RUN_WRITE_TESTS") == "1"
|
|
27
|
+
recipe_raw = (os.environ.get("DELULA_SDK_TEST_RECIPE_ID") or "").strip()
|
|
28
|
+
recipe_id = int(recipe_raw) if recipe_raw.isdigit() else None
|
|
29
|
+
return api_key, base_url, run_write, recipe_id
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DelulaSdkIntegrationTests(unittest.TestCase):
|
|
33
|
+
@classmethod
|
|
34
|
+
def setUpClass(cls) -> None:
|
|
35
|
+
api_key, base_url, run_write, recipe_id = _load_config()
|
|
36
|
+
cls.api_key = api_key
|
|
37
|
+
cls.base_url = base_url
|
|
38
|
+
cls.run_write = run_write
|
|
39
|
+
cls.recipe_id = recipe_id
|
|
40
|
+
cls.client = DelulaClient(api_key, base_url=base_url)
|
|
41
|
+
|
|
42
|
+
def test_invalid_api_key(self) -> None:
|
|
43
|
+
bad = DelulaClient("dlu_invalid_key_for_test", base_url=self.base_url)
|
|
44
|
+
with self.assertRaises(DelulaApiError) as ctx:
|
|
45
|
+
bad.get_account()
|
|
46
|
+
self.assertEqual(ctx.exception.status, 401)
|
|
47
|
+
self.assertEqual(ctx.exception.code, "invalid_api_key")
|
|
48
|
+
|
|
49
|
+
def test_get_account(self) -> None:
|
|
50
|
+
account = self.client.get_account()
|
|
51
|
+
self.assertIsInstance(account.get("userId"), str)
|
|
52
|
+
self.assertTrue(account["userId"])
|
|
53
|
+
|
|
54
|
+
def test_get_credits(self) -> None:
|
|
55
|
+
credits = self.client.get_credits()
|
|
56
|
+
for key in ("totalCredits", "premiumCredits", "earnedCredits", "apiEligibleCredits"):
|
|
57
|
+
self.assertIn(key, credits)
|
|
58
|
+
self.assertIsInstance(credits[key], int)
|
|
59
|
+
self.assertEqual(credits["apiEligibleCredits"], credits["premiumCredits"])
|
|
60
|
+
breakdown = credits.get("breakdown") or {}
|
|
61
|
+
self.assertIn("premium", breakdown)
|
|
62
|
+
self.assertIn("earned", breakdown)
|
|
63
|
+
|
|
64
|
+
def test_list_recipes(self) -> None:
|
|
65
|
+
recipes = self.client.list_recipes().get("recipes") or []
|
|
66
|
+
self.assertGreater(len(recipes), 0)
|
|
67
|
+
for recipe in recipes[:5]:
|
|
68
|
+
self.assertIsInstance(recipe.get("id"), int)
|
|
69
|
+
self.assertIsInstance(recipe.get("name"), str)
|
|
70
|
+
self.assertIsInstance(recipe.get("creditCost"), int)
|
|
71
|
+
|
|
72
|
+
def test_get_recipe(self) -> None:
|
|
73
|
+
recipes = self.client.list_recipes().get("recipes") or []
|
|
74
|
+
pick = pick_cheapest_recipe(recipes, self.recipe_id)
|
|
75
|
+
detail = self.client.get_recipe(pick["id"])
|
|
76
|
+
self.assertEqual(detail["id"], pick["id"])
|
|
77
|
+
self.assertIsInstance(detail.get("name"), str)
|
|
78
|
+
self.assertEqual(detail["creditCost"], pick["creditCost"])
|
|
79
|
+
|
|
80
|
+
def test_list_generations(self) -> None:
|
|
81
|
+
result = self.client.list_generations(page=1, limit=5)
|
|
82
|
+
self.assertIn("generations", result)
|
|
83
|
+
self.assertIn("pagination", result)
|
|
84
|
+
self.assertIsInstance(result["generations"], list)
|
|
85
|
+
self.assertIsInstance(result["pagination"].get("page"), int)
|
|
86
|
+
|
|
87
|
+
def test_preflight_generation(self) -> None:
|
|
88
|
+
recipes = self.client.list_recipes().get("recipes") or []
|
|
89
|
+
pick = pick_cheapest_recipe(recipes, self.recipe_id)
|
|
90
|
+
detail = self.client.get_recipe(pick["id"])
|
|
91
|
+
form_data = build_minimal_form_data(detail)
|
|
92
|
+
result = self.client.preflight_generation(pick["id"], form_data)
|
|
93
|
+
self.assertTrue(result.get("passed"))
|
|
94
|
+
|
|
95
|
+
def test_recipe_not_found(self) -> None:
|
|
96
|
+
with self.assertRaises(DelulaApiError) as ctx:
|
|
97
|
+
self.client.get_recipe(999999999)
|
|
98
|
+
self.assertEqual(ctx.exception.status, 404)
|
|
99
|
+
self.assertEqual(ctx.exception.code, "recipe_not_found")
|
|
100
|
+
|
|
101
|
+
@unittest.skipUnless(os.environ.get("DELULA_SDK_RUN_WRITE_TESTS") == "1", "write tests disabled")
|
|
102
|
+
def test_create_generation_idempotent(self) -> None:
|
|
103
|
+
credits = self.client.get_credits()
|
|
104
|
+
recipes = self.client.list_recipes().get("recipes") or []
|
|
105
|
+
pick = pick_cheapest_recipe(recipes, self.recipe_id)
|
|
106
|
+
detail = self.client.get_recipe(pick["id"])
|
|
107
|
+
cost = detail["creditCost"]
|
|
108
|
+
if credits.get("premiumCredits", 0) < cost:
|
|
109
|
+
self.skipTest(f"Need {cost} premium credits; have {credits.get('premiumCredits', 0)}")
|
|
110
|
+
|
|
111
|
+
form_data = build_minimal_form_data(detail)
|
|
112
|
+
key = str(uuid.uuid4())
|
|
113
|
+
first = self.client.create_generation(pick["id"], form_data=form_data, idempotency_key=key)
|
|
114
|
+
self.assertIsInstance(first.get("generationId"), int)
|
|
115
|
+
second = self.client.create_generation(pick["id"], form_data=form_data, idempotency_key=key)
|
|
116
|
+
self.assertEqual(first.get("generationId"), second.get("generationId"))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
unittest.main()
|