llamagen-python 0.1.0__tar.gz → 0.1.2__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.
Files changed (20) hide show
  1. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/.gitignore +1 -0
  2. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/PKG-INFO +18 -31
  3. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/README.md +17 -30
  4. llamagen_python-0.1.2/Untitled.ipynb +126 -0
  5. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/pyproject.toml +1 -1
  6. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/src/llamagen/__init__.py +8 -2
  7. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/src/llamagen/_errors.py +4 -0
  8. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/src/llamagen/_http.py +22 -1
  9. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/tests/test_client.py +35 -1
  10. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/LICENSE +0 -0
  11. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/docs/RELEASE.md +0 -0
  12. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/examples/quickstart.py +0 -0
  13. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/src/llamagen/_client.py +0 -0
  14. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/src/llamagen/_types.py +0 -0
  15. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/src/llamagen/_webhooks.py +0 -0
  16. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/src/llamagen/py.typed +0 -0
  17. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/src/llamagen/resources/__init__.py +0 -0
  18. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/src/llamagen/resources/animations.py +0 -0
  19. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/src/llamagen/resources/comics.py +0 -0
  20. {llamagen_python-0.1.0 → llamagen_python-0.1.2}/tests/test_webhooks.py +0 -0
@@ -7,4 +7,5 @@
7
7
  /.mypy_cache/
8
8
  /.venv/
9
9
  __pycache__/
10
+ .ipynb_checkpoints/
10
11
  *.py[cod]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llamagen-python
3
- Version: 0.1.0
3
+ Version: 0.1.2
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,41 +155,28 @@ 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
- ## Publishing
159
-
160
- Detailed release notes are in [`docs/RELEASE.md`](docs/RELEASE.md).
161
-
162
- 1. Update `version` in `pyproject.toml`.
163
- 2. Run tests and packaging checks:
158
+ ## Errors
164
159
 
165
- ```bash
166
- python -m unittest discover -s tests
167
- python -m pip install --upgrade build twine
168
- python -m build
169
- python -m twine check dist/*
170
- ```
171
-
172
- 3. Publish to TestPyPI first:
173
-
174
- ```bash
175
- python -m twine upload --repository testpypi dist/*
176
- ```
177
-
178
- 4. Install from TestPyPI in a clean environment and smoke-test:
179
-
180
- ```bash
181
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ llamagen-python
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)
182
171
  ```
183
172
 
184
- 5. Publish to PyPI:
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.
185
176
 
186
- ```bash
187
- python -m twine upload dist/*
188
- ```
177
+ ## Publishing
189
178
 
190
- Use a PyPI API token through `TWINE_USERNAME=__token__` and
191
- `TWINE_PASSWORD=pypi-...` or configure a trusted publisher in PyPI for GitHub
192
- Actions.
179
+ Maintainers can follow the release guide in [`docs/RELEASE.md`](docs/RELEASE.md).
193
180
 
194
181
  ## Local Dev
195
182
 
@@ -130,41 +130,28 @@ 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
- ## Publishing
134
-
135
- Detailed release notes are in [`docs/RELEASE.md`](docs/RELEASE.md).
136
-
137
- 1. Update `version` in `pyproject.toml`.
138
- 2. Run tests and packaging checks:
133
+ ## Errors
139
134
 
140
- ```bash
141
- python -m unittest discover -s tests
142
- python -m pip install --upgrade build twine
143
- python -m build
144
- python -m twine check dist/*
145
- ```
146
-
147
- 3. Publish to TestPyPI first:
148
-
149
- ```bash
150
- python -m twine upload --repository testpypi dist/*
151
- ```
152
-
153
- 4. Install from TestPyPI in a clean environment and smoke-test:
154
-
155
- ```bash
156
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ llamagen-python
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)
157
146
  ```
158
147
 
159
- 5. Publish to PyPI:
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.
160
151
 
161
- ```bash
162
- python -m twine upload dist/*
163
- ```
152
+ ## Publishing
164
153
 
165
- Use a PyPI API token through `TWINE_USERNAME=__token__` and
166
- `TWINE_PASSWORD=pypi-...` or configure a trusted publisher in PyPI for GitHub
167
- Actions.
154
+ Maintainers can follow the release guide in [`docs/RELEASE.md`](docs/RELEASE.md).
168
155
 
169
156
  ## Local Dev
170
157
 
@@ -0,0 +1,126 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 6,
6
+ "id": "83b6314c-08bb-4dcb-b62b-7e362a5aa15b",
7
+ "metadata": {},
8
+ "outputs": [
9
+ {
10
+ "ename": "LlamaGenAPIError",
11
+ "evalue": "LlamaGen API request failed with status 403",
12
+ "output_type": "error",
13
+ "traceback": [
14
+ "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
15
+ "\u001b[31mLlamaGenAPIError\u001b[39m Traceback (most recent call last)",
16
+ "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[6]\u001b[39m\u001b[32m, line 6\u001b[39m\n\u001b[32m 2\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m llamagen \u001b[38;5;28;01mimport\u001b[39;00m LlamaGenClient\n\u001b[32m 3\u001b[39m \n\u001b[32m 4\u001b[39m llamagen = LlamaGenClient(api_key=\u001b[33m'sk-5d609cd64379d048edcb04dcc109a6cfaf7c066f12138c13c8c404766ab9629d'\u001b[39m)\n\u001b[32m 5\u001b[39m \n\u001b[32m----> \u001b[39m\u001b[32m6\u001b[39m created = llamagen.comic.create({\n\u001b[32m 7\u001b[39m \u001b[33m\"prompt\"\u001b[39m: \u001b[33m\"A 4-panel comic about Leo finding a glowing key in a quiet library.\"\u001b[39m,\n\u001b[32m 8\u001b[39m \u001b[33m\"style\"\u001b[39m: \u001b[33m\"manga\"\u001b[39m,\n\u001b[32m 9\u001b[39m \u001b[33m\"fixPanelNum\"\u001b[39m: \u001b[32m4\u001b[39m,\n",
17
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/site-packages/llamagen/resources/comics.py:35\u001b[39m, in \u001b[36mComicsResource.create\u001b[39m\u001b[34m(self, params)\u001b[39m\n\u001b[32m 33\u001b[39m body: Dict[\u001b[38;5;28mstr\u001b[39m, Any] = {\u001b[33m\"\u001b[39m\u001b[33mpreset\u001b[39m\u001b[33m\"\u001b[39m: DEFAULT_PRESET, \u001b[33m\"\u001b[39m\u001b[33msize\u001b[39m\u001b[33m\"\u001b[39m: DEFAULT_SIZE}\n\u001b[32m 34\u001b[39m body.update(params)\n\u001b[32m---> \u001b[39m\u001b[32m35\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_http\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m\"\u001b[39;49m\u001b[30;43m/comics/generations\u001b[39;49m\u001b[30;43m\"\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mmethod\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43m\"\u001b[39;49m\u001b[30;43mPOST\u001b[39;49m\u001b[30;43m\"\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mjson_body\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mbody\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
18
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/site-packages/llamagen/_http.py:105\u001b[39m, in \u001b[36mHTTPClient.request\u001b[39m\u001b[34m(self, path, method, json_body, body, headers)\u001b[39m\n\u001b[32m 102\u001b[39m time.sleep(\u001b[38;5;28mself\u001b[39m.retry_delay * attempt)\n\u001b[32m 103\u001b[39m \u001b[38;5;28;01mcontinue\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m105\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m LlamaGenAPIError(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mLlamaGen API request failed with status \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstatus\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m, status, data)\n",
19
+ "\u001b[31mLlamaGenAPIError\u001b[39m: LlamaGen API request failed with status 403"
20
+ ]
21
+ }
22
+ ],
23
+ "source": [
24
+ "import os\n",
25
+ "from llamagen import LlamaGenClient\n",
26
+ "\n",
27
+ "llamagen = LlamaGenClient(api_key='sk-5d609cd64379d048edcb04dcc109a6cfaf7c066f12138c13c8c404766ab9629d')\n",
28
+ "\n",
29
+ "created = llamagen.comic.create({\n",
30
+ " \"prompt\": \"A 4-panel comic about Leo finding a glowing key in a quiet library.\",\n",
31
+ " \"style\": \"manga\",\n",
32
+ " \"fixPanelNum\": 4,\n",
33
+ "})\n",
34
+ "\n",
35
+ "done = llamagen.comic.wait_for_completion(created[\"id\"])\n",
36
+ "print(done[\"status\"], done.get(\"comics\"))"
37
+ ]
38
+ },
39
+ {
40
+ "cell_type": "code",
41
+ "execution_count": 7,
42
+ "id": "34cf0202-a209-4f09-b16e-eca092d7a0c4",
43
+ "metadata": {},
44
+ "outputs": [
45
+ {
46
+ "ename": "URLError",
47
+ "evalue": "<urlopen error [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1000)>",
48
+ "output_type": "error",
49
+ "traceback": [
50
+ "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
51
+ "\u001b[31mSSLEOFError\u001b[39m Traceback (most recent call last)",
52
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/urllib/request.py:1344\u001b[39m, in \u001b[36mAbstractHTTPHandler.do_open\u001b[39m\u001b[34m(self, http_class, req, **http_conn_args)\u001b[39m\n\u001b[32m 1343\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m1344\u001b[39m \u001b[30;43mh\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mreq\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mget_method\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mreq\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mselector\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mreq\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mdata\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mheaders\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 1345\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mencode_chunked\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mreq\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mhas_header\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m'\u001b[39;49m\u001b[30;43mTransfer-encoding\u001b[39;49m\u001b[30;43m'\u001b[39;49m\u001b[30;43m)\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 1346\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mOSError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m err: \u001b[38;5;66;03m# timeout error\u001b[39;00m\n",
53
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/http/client.py:1336\u001b[39m, in \u001b[36mHTTPConnection.request\u001b[39m\u001b[34m(self, method, url, body, headers, encode_chunked)\u001b[39m\n\u001b[32m 1335\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Send a complete request to the server.\"\"\"\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m1336\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_send_request\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mmethod\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43murl\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mbody\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mheaders\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mencode_chunked\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
54
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/http/client.py:1382\u001b[39m, in \u001b[36mHTTPConnection._send_request\u001b[39m\u001b[34m(self, method, url, body, headers, encode_chunked)\u001b[39m\n\u001b[32m 1381\u001b[39m body = _encode(body, \u001b[33m'\u001b[39m\u001b[33mbody\u001b[39m\u001b[33m'\u001b[39m)\n\u001b[32m-> \u001b[39m\u001b[32m1382\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mendheaders\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mbody\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mencode_chunked\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mencode_chunked\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
55
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/http/client.py:1331\u001b[39m, in \u001b[36mHTTPConnection.endheaders\u001b[39m\u001b[34m(self, message_body, encode_chunked)\u001b[39m\n\u001b[32m 1330\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m CannotSendHeader()\n\u001b[32m-> \u001b[39m\u001b[32m1331\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_send_output\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mmessage_body\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mencode_chunked\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mencode_chunked\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
56
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/http/client.py:1091\u001b[39m, in \u001b[36mHTTPConnection._send_output\u001b[39m\u001b[34m(self, message_body, encode_chunked)\u001b[39m\n\u001b[32m 1090\u001b[39m \u001b[38;5;28;01mdel\u001b[39;00m \u001b[38;5;28mself\u001b[39m._buffer[:]\n\u001b[32m-> \u001b[39m\u001b[32m1091\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43msend\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mmsg\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 1093\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m message_body \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 1094\u001b[39m \n\u001b[32m 1095\u001b[39m \u001b[38;5;66;03m# create a consistent interface to message_body\u001b[39;00m\n",
57
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/http/client.py:1035\u001b[39m, in \u001b[36mHTTPConnection.send\u001b[39m\u001b[34m(self, data)\u001b[39m\n\u001b[32m 1034\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.auto_open:\n\u001b[32m-> \u001b[39m\u001b[32m1035\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mconnect\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 1036\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n",
58
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/http/client.py:1477\u001b[39m, in \u001b[36mHTTPSConnection.connect\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 1475\u001b[39m server_hostname = \u001b[38;5;28mself\u001b[39m.host\n\u001b[32m-> \u001b[39m\u001b[32m1477\u001b[39m \u001b[38;5;28mself\u001b[39m.sock = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_context\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mwrap_socket\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43msock\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 1478\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mserver_hostname\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mserver_hostname\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
59
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/ssl.py:455\u001b[39m, in \u001b[36mSSLContext.wrap_socket\u001b[39m\u001b[34m(self, sock, server_side, do_handshake_on_connect, suppress_ragged_eofs, server_hostname, session)\u001b[39m\n\u001b[32m 449\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mwrap_socket\u001b[39m(\u001b[38;5;28mself\u001b[39m, sock, server_side=\u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[32m 450\u001b[39m do_handshake_on_connect=\u001b[38;5;28;01mTrue\u001b[39;00m,\n\u001b[32m 451\u001b[39m suppress_ragged_eofs=\u001b[38;5;28;01mTrue\u001b[39;00m,\n\u001b[32m 452\u001b[39m server_hostname=\u001b[38;5;28;01mNone\u001b[39;00m, session=\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[32m 453\u001b[39m \u001b[38;5;66;03m# SSLSocket class handles server_hostname encoding before it calls\u001b[39;00m\n\u001b[32m 454\u001b[39m \u001b[38;5;66;03m# ctx._wrap_socket()\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m455\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43msslsocket_class\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_create\u001b[39;49m\u001b[30;43m(\u001b[39;49m\n\u001b[32m 456\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43msock\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43msock\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 457\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mserver_side\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mserver_side\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 458\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mdo_handshake_on_connect\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mdo_handshake_on_connect\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 459\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43msuppress_ragged_eofs\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43msuppress_ragged_eofs\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 460\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mserver_hostname\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mserver_hostname\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 461\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mcontext\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 462\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43msession\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43msession\u001b[39;49m\n\u001b[32m 463\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
60
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/ssl.py:1041\u001b[39m, in \u001b[36mSSLSocket._create\u001b[39m\u001b[34m(cls, sock, server_side, do_handshake_on_connect, suppress_ragged_eofs, server_hostname, context, session)\u001b[39m\n\u001b[32m 1040\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mdo_handshake_on_connect should not be specified for non-blocking sockets\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m-> \u001b[39m\u001b[32m1041\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mdo_handshake\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 1042\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m:\n",
61
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/ssl.py:1319\u001b[39m, in \u001b[36mSSLSocket.do_handshake\u001b[39m\u001b[34m(self, block)\u001b[39m\n\u001b[32m 1318\u001b[39m \u001b[38;5;28mself\u001b[39m.settimeout(\u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[32m-> \u001b[39m\u001b[32m1319\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_sslobj\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mdo_handshake\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 1320\u001b[39m \u001b[38;5;28;01mfinally\u001b[39;00m:\n",
62
+ "\u001b[31mSSLEOFError\u001b[39m: [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1000)",
63
+ "\nDuring handling of the above exception, another exception occurred:\n",
64
+ "\u001b[31mURLError\u001b[39m Traceback (most recent call last)",
65
+ "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 12\u001b[39m\n\u001b[32m 8\u001b[39m \u001b[33m\"style\"\u001b[39m: \u001b[33m\"manga\"\u001b[39m,\n\u001b[32m 9\u001b[39m \u001b[33m\"fixPanelNum\"\u001b[39m: \u001b[32m4\u001b[39m,\n\u001b[32m 10\u001b[39m })\n\u001b[32m 11\u001b[39m print(created)\n\u001b[32m---> \u001b[39m\u001b[32m12\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m LlamaGenAPIError \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m 13\u001b[39m print(\u001b[33m\"status:\"\u001b[39m, e.status)\n\u001b[32m 14\u001b[39m print(\u001b[33m\"data:\"\u001b[39m, e.data)\n",
66
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/site-packages/llamagen/resources/comics.py:35\u001b[39m, in \u001b[36mComicsResource.create\u001b[39m\u001b[34m(self, params)\u001b[39m\n\u001b[32m 33\u001b[39m body: Dict[\u001b[38;5;28mstr\u001b[39m, Any] = {\u001b[33m\"\u001b[39m\u001b[33mpreset\u001b[39m\u001b[33m\"\u001b[39m: DEFAULT_PRESET, \u001b[33m\"\u001b[39m\u001b[33msize\u001b[39m\u001b[33m\"\u001b[39m: DEFAULT_SIZE}\n\u001b[32m 34\u001b[39m body.update(params)\n\u001b[32m---> \u001b[39m\u001b[32m35\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_http\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m\"\u001b[39;49m\u001b[30;43m/comics/generations\u001b[39;49m\u001b[30;43m\"\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mmethod\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43m\"\u001b[39;49m\u001b[30;43mPOST\u001b[39;49m\u001b[30;43m\"\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mjson_body\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mbody\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
67
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/site-packages/llamagen/_http.py:84\u001b[39m, in \u001b[36mHTTPClient.request\u001b[39m\u001b[34m(self, path, method, json_body, body, headers)\u001b[39m\n\u001b[32m 82\u001b[39m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[32m 83\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m84\u001b[39m status, response_headers, response_body = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mtransport\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m(\u001b[39;49m\n\u001b[32m 85\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mmethod\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 86\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43murl\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 87\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mprepared_headers\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 88\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mbody\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 89\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mtimeout\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 90\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 91\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m error:\n\u001b[32m 92\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m _is_timeout_error(error):\n",
68
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/site-packages/llamagen/_http.py:36\u001b[39m, in \u001b[36mUrllibTransport.request\u001b[39m\u001b[34m(self, method, url, headers, body, timeout)\u001b[39m\n\u001b[32m 34\u001b[39m request = urllib.request.Request(url, data=body, method=method, headers=\u001b[38;5;28mdict\u001b[39m(headers))\n\u001b[32m 35\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m36\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[30;43murllib\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43murlopen\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mtimeout\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mtimeout\u001b[39;49m\u001b[30;43m)\u001b[39;49m \u001b[38;5;28;01mas\u001b[39;00m response:\n\u001b[32m 37\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m response.status, \u001b[38;5;28mdict\u001b[39m(response.headers.items()), response.read()\n\u001b[32m 38\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m urllib.error.HTTPError \u001b[38;5;28;01mas\u001b[39;00m error:\n",
69
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/urllib/request.py:215\u001b[39m, in \u001b[36murlopen\u001b[39m\u001b[34m(url, data, timeout, cafile, capath, cadefault, context)\u001b[39m\n\u001b[32m 213\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 214\u001b[39m opener = _opener\n\u001b[32m--> \u001b[39m\u001b[32m215\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mopener\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mopen\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43murl\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mdata\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mtimeout\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
70
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/urllib/request.py:515\u001b[39m, in \u001b[36mOpenerDirector.open\u001b[39m\u001b[34m(self, fullurl, data, timeout)\u001b[39m\n\u001b[32m 512\u001b[39m req = meth(req)\n\u001b[32m 514\u001b[39m sys.audit(\u001b[33m'\u001b[39m\u001b[33murllib.Request\u001b[39m\u001b[33m'\u001b[39m, req.full_url, req.data, req.headers, req.get_method())\n\u001b[32m--> \u001b[39m\u001b[32m515\u001b[39m response = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_open\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mreq\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mdata\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 517\u001b[39m \u001b[38;5;66;03m# post-process response\u001b[39;00m\n\u001b[32m 518\u001b[39m meth_name = protocol+\u001b[33m\"\u001b[39m\u001b[33m_response\u001b[39m\u001b[33m\"\u001b[39m\n",
71
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/urllib/request.py:532\u001b[39m, in \u001b[36mOpenerDirector._open\u001b[39m\u001b[34m(self, req, data)\u001b[39m\n\u001b[32m 529\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m result\n\u001b[32m 531\u001b[39m protocol = req.type\n\u001b[32m--> \u001b[39m\u001b[32m532\u001b[39m result = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_call_chain\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mhandle_open\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mprotocol\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mprotocol\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m+\u001b[39;49m\n\u001b[32m 533\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43m'\u001b[39;49m\u001b[30;43m_open\u001b[39;49m\u001b[30;43m'\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mreq\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 534\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m result:\n\u001b[32m 535\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m result\n",
72
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/urllib/request.py:492\u001b[39m, in \u001b[36mOpenerDirector._call_chain\u001b[39m\u001b[34m(self, chain, kind, meth_name, *args)\u001b[39m\n\u001b[32m 490\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m handler \u001b[38;5;129;01min\u001b[39;00m handlers:\n\u001b[32m 491\u001b[39m func = \u001b[38;5;28mgetattr\u001b[39m(handler, meth_name)\n\u001b[32m--> \u001b[39m\u001b[32m492\u001b[39m result = \u001b[30;43mfunc\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43margs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 493\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m result \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 494\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m result\n",
73
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/urllib/request.py:1392\u001b[39m, in \u001b[36mHTTPSHandler.https_open\u001b[39m\u001b[34m(self, req)\u001b[39m\n\u001b[32m 1391\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mhttps_open\u001b[39m(\u001b[38;5;28mself\u001b[39m, req):\n\u001b[32m-> \u001b[39m\u001b[32m1392\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mdo_open\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mhttp\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mclient\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mHTTPSConnection\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mreq\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 1393\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mcontext\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_context\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
74
+ "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/lib/python3.12/urllib/request.py:1347\u001b[39m, in \u001b[36mAbstractHTTPHandler.do_open\u001b[39m\u001b[34m(self, http_class, req, **http_conn_args)\u001b[39m\n\u001b[32m 1344\u001b[39m h.request(req.get_method(), req.selector, req.data, headers,\n\u001b[32m 1345\u001b[39m encode_chunked=req.has_header(\u001b[33m'\u001b[39m\u001b[33mTransfer-encoding\u001b[39m\u001b[33m'\u001b[39m))\n\u001b[32m 1346\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mOSError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m err: \u001b[38;5;66;03m# timeout error\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m1347\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m URLError(err)\n\u001b[32m 1348\u001b[39m r = h.getresponse()\n\u001b[32m 1349\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m:\n",
75
+ "\u001b[31mURLError\u001b[39m: <urlopen error [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1000)>"
76
+ ]
77
+ }
78
+ ],
79
+ "source": [
80
+ "from llamagen import LlamaGenClient, LlamaGenAPIError\n",
81
+ "\n",
82
+ "client = LlamaGenClient(api_key=\"sk-5d609cd64379d048edcb04dcc109a6cfaf7c066f12138c13c8c404766ab9629d\")\n",
83
+ "\n",
84
+ "try:\n",
85
+ " created = client.comic.create({\n",
86
+ " \"prompt\": \"A 4-panel comic about Leo finding a glowing key in a quiet library.\",\n",
87
+ " \"style\": \"manga\",\n",
88
+ " \"fixPanelNum\": 4,\n",
89
+ " })\n",
90
+ " print(created)\n",
91
+ "except LlamaGenAPIError as e:\n",
92
+ " print(\"status:\", e.status)\n",
93
+ " print(\"data:\", e.data)"
94
+ ]
95
+ },
96
+ {
97
+ "cell_type": "code",
98
+ "execution_count": null,
99
+ "id": "0b238159-ea0a-4a41-9c57-0ca986456db4",
100
+ "metadata": {},
101
+ "outputs": [],
102
+ "source": []
103
+ }
104
+ ],
105
+ "metadata": {
106
+ "kernelspec": {
107
+ "display_name": "Python 3 (ipykernel)",
108
+ "language": "python",
109
+ "name": "python3"
110
+ },
111
+ "language_info": {
112
+ "codemirror_mode": {
113
+ "name": "ipython",
114
+ "version": 3
115
+ },
116
+ "file_extension": ".py",
117
+ "mimetype": "text/x-python",
118
+ "name": "python",
119
+ "nbconvert_exporter": "python",
120
+ "pygments_lexer": "ipython3",
121
+ "version": "3.12.7"
122
+ }
123
+ },
124
+ "nbformat": 4,
125
+ "nbformat_minor": 5
126
+ }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "llamagen-python"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Official Python SDK for LlamaGen Comic API and Animation API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,11 +1,17 @@
1
1
  from ._client import LlamaGenClient
2
- from ._errors import LlamaGenAPIError, LlamaGenTimeoutError, LlamaGenWebhookSignatureError
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.0"
22
+ __version__ = "0.1.2"
@@ -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,13 @@ 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
11
12
 
12
13
 
13
14
  class Transport(Protocol):
@@ -90,7 +91,19 @@ class HTTPClient:
90
91
  )
91
92
  except Exception as error:
92
93
  if _is_timeout_error(error):
94
+ if attempt < self.max_retries:
95
+ attempt += 1
96
+ time.sleep(self.retry_delay * attempt)
97
+ continue
93
98
  raise LlamaGenTimeoutError(f"Request timed out after {int(self.timeout * 1000)}ms") from error
99
+ if _is_connection_error(error):
100
+ if attempt < self.max_retries:
101
+ attempt += 1
102
+ time.sleep(self.retry_delay * attempt)
103
+ continue
104
+ raise LlamaGenConnectionError(
105
+ f"Could not connect to LlamaGen API after {attempt + 1} attempts: {error}"
106
+ ) from error
94
107
  raise
95
108
 
96
109
  data = _decode_response(response_body, response_headers)
@@ -135,3 +148,11 @@ def _is_timeout_error(error: BaseException) -> bool:
135
148
  if isinstance(error, urllib.error.URLError):
136
149
  return isinstance(error.reason, (TimeoutError, socket.timeout))
137
150
  return False
151
+
152
+
153
+ def _is_connection_error(error: BaseException) -> bool:
154
+ if isinstance(error, (ConnectionError, ssl.SSLError)):
155
+ return True
156
+ if isinstance(error, urllib.error.URLError):
157
+ return not _is_timeout_error(error)
158
+ 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"})])
@@ -86,6 +105,21 @@ class ClientTests(unittest.TestCase):
86
105
  body = json.loads(transport.calls[0]["body"].decode("utf-8"))
87
106
  self.assertEqual(body["action"], "regeneratePanel")
88
107
 
108
+ def test_retries_ssl_url_errors_and_raises_connection_error(self):
109
+ transport = FailingTransport(
110
+ [
111
+ urllib.error.URLError(ssl.SSLEOFError("unexpected eof")),
112
+ urllib.error.URLError(ssl.SSLEOFError("unexpected eof")),
113
+ urllib.error.URLError(ssl.SSLEOFError("unexpected eof")),
114
+ ]
115
+ )
116
+ client = LlamaGenClient(api_key="test-key", retry_delay_ms=1, transport=transport)
117
+
118
+ with self.assertRaises(LlamaGenConnectionError):
119
+ client.comic.usage()
120
+
121
+ self.assertEqual(len(transport.calls), 3)
122
+
89
123
 
90
124
  if __name__ == "__main__":
91
125
  unittest.main()
File without changes