llamagen-python 0.1.1__tar.gz → 0.1.3__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.
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/.gitignore +1 -0
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/PKG-INFO +20 -1
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/README.md +19 -0
- llamagen_python-0.1.3/examples/quickstart.py +23 -0
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/pyproject.toml +1 -1
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/src/llamagen/__init__.py +8 -2
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/src/llamagen/_errors.py +4 -0
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/src/llamagen/_http.py +29 -2
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/tests/test_client.py +37 -1
- llamagen_python-0.1.1/examples/quickstart.py +0 -17
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/LICENSE +0 -0
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/docs/RELEASE.md +0 -0
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/src/llamagen/_client.py +0 -0
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/src/llamagen/_types.py +0 -0
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/src/llamagen/_webhooks.py +0 -0
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/src/llamagen/py.typed +0 -0
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/src/llamagen/resources/__init__.py +0 -0
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/src/llamagen/resources/animations.py +0 -0
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/src/llamagen/resources/comics.py +0 -0
- {llamagen_python-0.1.1 → llamagen_python-0.1.3}/tests/test_webhooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: llamagen-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Official Python SDK for LlamaGen Comic API and Animation API
|
|
5
5
|
Project-URL: Homepage, https://llamagen.ai/comic-api
|
|
6
6
|
Project-URL: Documentation, https://llamagen.ai/comic-api/docs
|
|
@@ -155,6 +155,25 @@ event = construct_webhook_event(
|
|
|
155
155
|
The helper verifies `X-Llama-Webhook-Timestamp` and
|
|
156
156
|
`X-Llama-Webhook-Signature` with HMAC SHA-256 and a default 5-minute tolerance.
|
|
157
157
|
|
|
158
|
+
## Errors
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from llamagen import LlamaGenAPIError, LlamaGenConnectionError, LlamaGenTimeoutError
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
usage = llamagen.comic.usage()
|
|
165
|
+
except LlamaGenAPIError as error:
|
|
166
|
+
print(error.status, error.data)
|
|
167
|
+
except LlamaGenConnectionError as error:
|
|
168
|
+
print("Network connection failed:", error)
|
|
169
|
+
except LlamaGenTimeoutError as error:
|
|
170
|
+
print("Request timed out:", error)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- `LlamaGenAPIError`: the API returned a non-2xx response, such as `401`, `403`, or `429`.
|
|
174
|
+
- `LlamaGenConnectionError`: the SDK could not connect to the API after retries.
|
|
175
|
+
- `LlamaGenTimeoutError`: the request timed out after retries.
|
|
176
|
+
|
|
158
177
|
## Publishing
|
|
159
178
|
|
|
160
179
|
Maintainers can follow the release guide in [`docs/RELEASE.md`](docs/RELEASE.md).
|
|
@@ -130,6 +130,25 @@ event = construct_webhook_event(
|
|
|
130
130
|
The helper verifies `X-Llama-Webhook-Timestamp` and
|
|
131
131
|
`X-Llama-Webhook-Signature` with HMAC SHA-256 and a default 5-minute tolerance.
|
|
132
132
|
|
|
133
|
+
## Errors
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from llamagen import LlamaGenAPIError, LlamaGenConnectionError, LlamaGenTimeoutError
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
usage = llamagen.comic.usage()
|
|
140
|
+
except LlamaGenAPIError as error:
|
|
141
|
+
print(error.status, error.data)
|
|
142
|
+
except LlamaGenConnectionError as error:
|
|
143
|
+
print("Network connection failed:", error)
|
|
144
|
+
except LlamaGenTimeoutError as error:
|
|
145
|
+
print("Request timed out:", error)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- `LlamaGenAPIError`: the API returned a non-2xx response, such as `401`, `403`, or `429`.
|
|
149
|
+
- `LlamaGenConnectionError`: the SDK could not connect to the API after retries.
|
|
150
|
+
- `LlamaGenTimeoutError`: the request timed out after retries.
|
|
151
|
+
|
|
133
152
|
## Publishing
|
|
134
153
|
|
|
135
154
|
Maintainers can follow the release guide in [`docs/RELEASE.md`](docs/RELEASE.md).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from llamagen import LlamaGenAPIError, LlamaGenConnectionError, LlamaGenClient, LlamaGenTimeoutError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
client = LlamaGenClient(api_key=os.environ["LLAMAGEN_API_KEY"])
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
created = client.comic.create(
|
|
10
|
+
{
|
|
11
|
+
"prompt": "A 4-panel comic about Leo finding a glowing key in a quiet library.",
|
|
12
|
+
"style": "manga",
|
|
13
|
+
"fixPanelNum": 4,
|
|
14
|
+
}
|
|
15
|
+
)
|
|
16
|
+
done = client.comic.wait_for_completion(created["id"])
|
|
17
|
+
print(done["status"])
|
|
18
|
+
except LlamaGenAPIError as error:
|
|
19
|
+
print("LlamaGen API error:", error.status, error.data)
|
|
20
|
+
except LlamaGenConnectionError as error:
|
|
21
|
+
print("LlamaGen connection error:", error)
|
|
22
|
+
except LlamaGenTimeoutError as error:
|
|
23
|
+
print("LlamaGen timeout:", error)
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
from ._client import LlamaGenClient
|
|
2
|
-
from ._errors import
|
|
2
|
+
from ._errors import (
|
|
3
|
+
LlamaGenAPIError,
|
|
4
|
+
LlamaGenConnectionError,
|
|
5
|
+
LlamaGenTimeoutError,
|
|
6
|
+
LlamaGenWebhookSignatureError,
|
|
7
|
+
)
|
|
3
8
|
from ._types import SUPPORTED_COMIC_SIZES
|
|
4
9
|
from ._webhooks import construct_webhook_event, verify_webhook_signature
|
|
5
10
|
|
|
6
11
|
__all__ = [
|
|
7
12
|
"LlamaGenAPIError",
|
|
8
13
|
"LlamaGenClient",
|
|
14
|
+
"LlamaGenConnectionError",
|
|
9
15
|
"LlamaGenTimeoutError",
|
|
10
16
|
"LlamaGenWebhookSignatureError",
|
|
11
17
|
"SUPPORTED_COMIC_SIZES",
|
|
@@ -13,4 +19,4 @@ __all__ = [
|
|
|
13
19
|
"verify_webhook_signature",
|
|
14
20
|
]
|
|
15
21
|
|
|
16
|
-
__version__ = "0.1.
|
|
22
|
+
__version__ = "0.1.3"
|
|
@@ -16,5 +16,9 @@ class LlamaGenTimeoutError(TimeoutError):
|
|
|
16
16
|
"""Raised when a request or polling operation times out."""
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
class LlamaGenConnectionError(ConnectionError):
|
|
20
|
+
"""Raised when the SDK cannot connect to the LlamaGen API."""
|
|
21
|
+
|
|
22
|
+
|
|
19
23
|
class LlamaGenWebhookSignatureError(ValueError):
|
|
20
24
|
"""Raised when webhook signature verification fails."""
|
|
@@ -2,12 +2,15 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import socket
|
|
5
|
+
import ssl
|
|
5
6
|
import time
|
|
6
7
|
import urllib.error
|
|
7
8
|
import urllib.request
|
|
8
9
|
from typing import Any, Dict, Mapping, Optional, Protocol, Tuple
|
|
9
10
|
|
|
10
|
-
from ._errors import LlamaGenAPIError, LlamaGenTimeoutError
|
|
11
|
+
from ._errors import LlamaGenAPIError, LlamaGenConnectionError, LlamaGenTimeoutError
|
|
12
|
+
|
|
13
|
+
DEFAULT_USER_AGENT = "llamagen-python/0.1.3"
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
class Transport(Protocol):
|
|
@@ -69,7 +72,11 @@ class HTTPClient:
|
|
|
69
72
|
if json_body is not None and body is not None:
|
|
70
73
|
raise ValueError("Pass either json_body or body, not both.")
|
|
71
74
|
|
|
72
|
-
prepared_headers: Dict[str, str] = {
|
|
75
|
+
prepared_headers: Dict[str, str] = {
|
|
76
|
+
"Accept": "application/json",
|
|
77
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
78
|
+
"User-Agent": DEFAULT_USER_AGENT,
|
|
79
|
+
}
|
|
73
80
|
if json_body is not None:
|
|
74
81
|
body = json.dumps(json_body, separators=(",", ":")).encode("utf-8")
|
|
75
82
|
prepared_headers["Content-Type"] = "application/json"
|
|
@@ -90,7 +97,19 @@ class HTTPClient:
|
|
|
90
97
|
)
|
|
91
98
|
except Exception as error:
|
|
92
99
|
if _is_timeout_error(error):
|
|
100
|
+
if attempt < self.max_retries:
|
|
101
|
+
attempt += 1
|
|
102
|
+
time.sleep(self.retry_delay * attempt)
|
|
103
|
+
continue
|
|
93
104
|
raise LlamaGenTimeoutError(f"Request timed out after {int(self.timeout * 1000)}ms") from error
|
|
105
|
+
if _is_connection_error(error):
|
|
106
|
+
if attempt < self.max_retries:
|
|
107
|
+
attempt += 1
|
|
108
|
+
time.sleep(self.retry_delay * attempt)
|
|
109
|
+
continue
|
|
110
|
+
raise LlamaGenConnectionError(
|
|
111
|
+
f"Could not connect to LlamaGen API after {attempt + 1} attempts: {error}"
|
|
112
|
+
) from error
|
|
94
113
|
raise
|
|
95
114
|
|
|
96
115
|
data = _decode_response(response_body, response_headers)
|
|
@@ -135,3 +154,11 @@ def _is_timeout_error(error: BaseException) -> bool:
|
|
|
135
154
|
if isinstance(error, urllib.error.URLError):
|
|
136
155
|
return isinstance(error.reason, (TimeoutError, socket.timeout))
|
|
137
156
|
return False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_connection_error(error: BaseException) -> bool:
|
|
160
|
+
if isinstance(error, (ConnectionError, ssl.SSLError)):
|
|
161
|
+
return True
|
|
162
|
+
if isinstance(error, urllib.error.URLError):
|
|
163
|
+
return not _is_timeout_error(error)
|
|
164
|
+
return False
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import ssl
|
|
4
5
|
import unittest
|
|
6
|
+
import urllib.error
|
|
5
7
|
from typing import Mapping, Optional, Tuple
|
|
6
8
|
|
|
7
|
-
from llamagen import LlamaGenAPIError, LlamaGenClient
|
|
9
|
+
from llamagen import LlamaGenAPIError, LlamaGenClient, LlamaGenConnectionError
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class FakeTransport:
|
|
@@ -25,6 +27,23 @@ class FakeTransport:
|
|
|
25
27
|
return status, {"content-type": "application/json"}, json.dumps(payload).encode("utf-8")
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
class FailingTransport:
|
|
31
|
+
def __init__(self, errors):
|
|
32
|
+
self.errors = list(errors)
|
|
33
|
+
self.calls = []
|
|
34
|
+
|
|
35
|
+
def request(
|
|
36
|
+
self,
|
|
37
|
+
method: str,
|
|
38
|
+
url: str,
|
|
39
|
+
headers: Mapping[str, str],
|
|
40
|
+
body: Optional[bytes],
|
|
41
|
+
timeout: float,
|
|
42
|
+
) -> Tuple[int, Mapping[str, str], bytes]:
|
|
43
|
+
self.calls.append({"method": method, "url": url, "headers": dict(headers), "body": body})
|
|
44
|
+
raise self.errors.pop(0)
|
|
45
|
+
|
|
46
|
+
|
|
28
47
|
class ClientTests(unittest.TestCase):
|
|
29
48
|
def test_create_comic_applies_defaults_and_auth(self):
|
|
30
49
|
transport = FakeTransport([(200, {"id": "gen_1", "status": "PENDING"})])
|
|
@@ -37,6 +56,8 @@ class ClientTests(unittest.TestCase):
|
|
|
37
56
|
self.assertEqual(call["method"], "POST")
|
|
38
57
|
self.assertEqual(call["url"], "https://api.llamagen.ai/v1/comics/generations")
|
|
39
58
|
self.assertEqual(call["headers"]["Authorization"], "Bearer test-key")
|
|
59
|
+
self.assertEqual(call["headers"]["Accept"], "application/json")
|
|
60
|
+
self.assertEqual(call["headers"]["User-Agent"], "llamagen-python/0.1.3")
|
|
40
61
|
body = json.loads(call["body"].decode("utf-8"))
|
|
41
62
|
self.assertEqual(body["preset"], "neutral")
|
|
42
63
|
self.assertEqual(body["size"], "1024x1024")
|
|
@@ -86,6 +107,21 @@ class ClientTests(unittest.TestCase):
|
|
|
86
107
|
body = json.loads(transport.calls[0]["body"].decode("utf-8"))
|
|
87
108
|
self.assertEqual(body["action"], "regeneratePanel")
|
|
88
109
|
|
|
110
|
+
def test_retries_ssl_url_errors_and_raises_connection_error(self):
|
|
111
|
+
transport = FailingTransport(
|
|
112
|
+
[
|
|
113
|
+
urllib.error.URLError(ssl.SSLEOFError("unexpected eof")),
|
|
114
|
+
urllib.error.URLError(ssl.SSLEOFError("unexpected eof")),
|
|
115
|
+
urllib.error.URLError(ssl.SSLEOFError("unexpected eof")),
|
|
116
|
+
]
|
|
117
|
+
)
|
|
118
|
+
client = LlamaGenClient(api_key="test-key", retry_delay_ms=1, transport=transport)
|
|
119
|
+
|
|
120
|
+
with self.assertRaises(LlamaGenConnectionError):
|
|
121
|
+
client.comic.usage()
|
|
122
|
+
|
|
123
|
+
self.assertEqual(len(transport.calls), 3)
|
|
124
|
+
|
|
89
125
|
|
|
90
126
|
if __name__ == "__main__":
|
|
91
127
|
unittest.main()
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
from llamagen import LlamaGenClient
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
client = LlamaGenClient(api_key=os.environ["LLAMAGEN_API_KEY"])
|
|
7
|
-
|
|
8
|
-
created = client.comic.create(
|
|
9
|
-
{
|
|
10
|
-
"prompt": "A 4-panel comic about Leo finding a glowing key in a quiet library.",
|
|
11
|
-
"style": "manga",
|
|
12
|
-
"fixPanelNum": 4,
|
|
13
|
-
}
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
done = client.comic.wait_for_completion(created["id"])
|
|
17
|
-
print(done["status"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|