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.
@@ -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,6 @@
1
+ """Delula Public API SDK (beta)."""
2
+
3
+ from delula_sdk.client import DelulaClient
4
+ from delula_sdk.errors import DelulaApiError
5
+
6
+ __all__ = ["DelulaClient", "DelulaApiError"]
@@ -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
+ 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()