picx-ai 0.2.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.
- picx_ai-0.2.0/.gitignore +45 -0
- picx_ai-0.2.0/LICENSE +21 -0
- picx_ai-0.2.0/PKG-INFO +289 -0
- picx_ai-0.2.0/README.md +233 -0
- picx_ai-0.2.0/examples/generate_image.py +82 -0
- picx_ai-0.2.0/examples/generate_video.py +72 -0
- picx_ai-0.2.0/pyproject.toml +84 -0
- picx_ai-0.2.0/src/picx/__init__.py +95 -0
- picx_ai-0.2.0/src/picx/_async_client.py +158 -0
- picx_ai-0.2.0/src/picx/_base.py +267 -0
- picx_ai-0.2.0/src/picx/_client.py +157 -0
- picx_ai-0.2.0/src/picx/_exceptions.py +152 -0
- picx_ai-0.2.0/src/picx/_jobs.py +143 -0
- picx_ai-0.2.0/src/picx/_params.py +239 -0
- picx_ai-0.2.0/src/picx/_redact.py +29 -0
- picx_ai-0.2.0/src/picx/_resources.py +421 -0
- picx_ai-0.2.0/src/picx/_resources_async.py +409 -0
- picx_ai-0.2.0/src/picx/_version.py +5 -0
- picx_ai-0.2.0/src/picx/py.typed +0 -0
- picx_ai-0.2.0/src/picx/types.py +436 -0
- picx_ai-0.2.0/tests/__init__.py +1 -0
- picx_ai-0.2.0/tests/conftest.py +125 -0
- picx_ai-0.2.0/tests/test_async_parity.py +211 -0
- picx_ai-0.2.0/tests/test_client.py +84 -0
- picx_ai-0.2.0/tests/test_errors.py +204 -0
- picx_ai-0.2.0/tests/test_images.py +128 -0
- picx_ai-0.2.0/tests/test_models_account.py +163 -0
- picx_ai-0.2.0/tests/test_polling.py +168 -0
- picx_ai-0.2.0/tests/test_redaction.py +124 -0
- picx_ai-0.2.0/tests/test_retries.py +179 -0
- picx_ai-0.2.0/tests/test_videos.py +136 -0
picx_ai-0.2.0/.gitignore
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
develop-eggs/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
sdist/
|
|
14
|
+
wheels/
|
|
15
|
+
*.egg-info/
|
|
16
|
+
*.egg
|
|
17
|
+
MANIFEST
|
|
18
|
+
|
|
19
|
+
# Virtual environments
|
|
20
|
+
.venv/
|
|
21
|
+
venv/
|
|
22
|
+
env/
|
|
23
|
+
ENV/
|
|
24
|
+
|
|
25
|
+
# Tooling caches
|
|
26
|
+
.mypy_cache/
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.ruff_cache/
|
|
29
|
+
.coverage
|
|
30
|
+
.coverage.*
|
|
31
|
+
htmlcov/
|
|
32
|
+
coverage.xml
|
|
33
|
+
.tox/
|
|
34
|
+
.nox/
|
|
35
|
+
|
|
36
|
+
# Editors / OS
|
|
37
|
+
.idea/
|
|
38
|
+
.vscode/
|
|
39
|
+
*.swp
|
|
40
|
+
.DS_Store
|
|
41
|
+
|
|
42
|
+
# Local secrets
|
|
43
|
+
.env
|
|
44
|
+
.env.*
|
|
45
|
+
!.env.example
|
picx_ai-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 PicX Studio
|
|
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.
|
picx_ai-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: picx-ai
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Official Python SDK for the PicX AI image and video generation API.
|
|
5
|
+
Project-URL: Homepage, https://picxstudio.com
|
|
6
|
+
Project-URL: Documentation, https://ai.picxstudio.com/docs/code-examples/python-sdk
|
|
7
|
+
Project-URL: Source, https://github.com/Type-Think-AI/picx-sdk-python
|
|
8
|
+
Project-URL: Issues, https://github.com/Type-Think-AI/picx-sdk-python/issues
|
|
9
|
+
Author-email: PicX Studio <support@picxstudio.com>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2025 PicX Studio
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: api,genai,image-generation,picx,sdk,video-generation
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: OS Independent
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
44
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
45
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
46
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
47
|
+
Classifier: Typing :: Typed
|
|
48
|
+
Requires-Python: >=3.9
|
|
49
|
+
Requires-Dist: httpx<1,>=0.24
|
|
50
|
+
Provides-Extra: dev
|
|
51
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
52
|
+
Requires-Dist: pytest-asyncio<1,>=0.24; extra == 'dev'
|
|
53
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
54
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
55
|
+
Description-Content-Type: text/markdown
|
|
56
|
+
|
|
57
|
+
# picx-ai
|
|
58
|
+
|
|
59
|
+
Official Python client for the [PicX](https://picxstudio.com) image and video generation API.
|
|
60
|
+
|
|
61
|
+
- Sync (`PicX`) and async (`AsyncPicX`) clients with the same surface
|
|
62
|
+
- Typed results and a typed exception hierarchy; ships `py.typed`, passes `mypy --strict`
|
|
63
|
+
- Automatic retries with exponential backoff + jitter, `Retry-After` aware
|
|
64
|
+
- `Idempotency-Key` support on the calls that spend credits
|
|
65
|
+
- One runtime dependency: [`httpx`](https://www.python-httpx.org/)
|
|
66
|
+
|
|
67
|
+
> **This is not the CLI.** This package is a library you install into your own
|
|
68
|
+
> application. The `admin-cli/` project in this organisation builds the internal
|
|
69
|
+
> `picx-admin` command line binary and is unrelated to this SDK.
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install picx-ai
|
|
75
|
+
# or
|
|
76
|
+
uv add picx-ai
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Requires Python 3.9+.
|
|
80
|
+
|
|
81
|
+
## Quickstart
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import os
|
|
85
|
+
from picx import PicX
|
|
86
|
+
|
|
87
|
+
picx = PicX(os.environ["PICX_API_KEY"])
|
|
88
|
+
job = picx.video.create(prompt="sneaker on marble, slow orbit", duration=12)
|
|
89
|
+
asset = job.wait()
|
|
90
|
+
print(asset.url)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`PicX()` falls back to the `PICX_API_KEY` environment variable when no key is
|
|
94
|
+
passed, so `PicX()` works once the variable is set. Use it as a context manager
|
|
95
|
+
to close the connection pool deterministically:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
with PicX() as picx:
|
|
99
|
+
asset = picx.images.generate("a red sneaker on white marble", size="2K", aspect_ratio="1:1")
|
|
100
|
+
print(asset.url, asset.credits_used)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Configuration
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
picx = PicX(
|
|
107
|
+
api_key="pxsk_...", # defaults to os.environ["PICX_API_KEY"]
|
|
108
|
+
base_url="https://api.picxstudio.com/v1", # or the PICX_BASE_URL env var
|
|
109
|
+
timeout=60.0, # seconds, per request
|
|
110
|
+
max_retries=2, # retries for 429 / 5xx / connection errors
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## API
|
|
115
|
+
|
|
116
|
+
| Call | Endpoint | Notes |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `picx.images.generate(prompt, model=…, size=…, aspect_ratio=…)` | `POST /images/generate` | scope `images:generate`; `size` is `1K`/`2K`/`4K`, `aspect_ratio` like `16:9` |
|
|
119
|
+
| `picx.images.edit(instruction, image_urls, model=…, size=…)` | `POST /images/edit` | scope `images:edit`; 1-5 image URLs |
|
|
120
|
+
| `picx.video.create(prompt=…, duration=…, resolution=…, sound=…, …)` | `POST /videos/generate` | scope `videos:generate`; returns **202** and a job to poll |
|
|
121
|
+
| `picx.generations.get(id)` | `GET /generations/{id}` | raises `NotFoundError` on 404 |
|
|
122
|
+
| `picx.models.list(type="image")` | `GET /models` | **public**, no API key needed |
|
|
123
|
+
| `picx.account.usage(period=30)` | `GET /account/usage` | |
|
|
124
|
+
| `picx.account.me()` | `GET /account/me` | |
|
|
125
|
+
|
|
126
|
+
`picx.images` is also available as `picx.image`, and `picx.video` as `picx.videos`.
|
|
127
|
+
|
|
128
|
+
### Images
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
asset = picx.images.generate("a red sneaker on white marble", size="2K", aspect_ratio="1:1")
|
|
132
|
+
asset.id, asset.url, asset.model, asset.size, asset.aspect_ratio, asset.credits_used
|
|
133
|
+
|
|
134
|
+
edited = picx.images.edit("put it on a wet street at night", [asset.url], size="4K")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Videos (202 Accepted, then poll)
|
|
138
|
+
|
|
139
|
+
`POST /videos/generate` is asynchronous server-side, so `create()` returns a job:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
job = picx.video.create(
|
|
143
|
+
prompt="sneaker on marble, slow orbit",
|
|
144
|
+
duration=12, # server default 5
|
|
145
|
+
resolution="720p", # server default "720p"; must be priced for the model
|
|
146
|
+
sound=True, # server default True
|
|
147
|
+
aspect_ratio="16:9",
|
|
148
|
+
mode="image", # "text" | "image" | "reference"
|
|
149
|
+
image_url="https://…/ref.png",
|
|
150
|
+
callback_url="https://example.com/webhook",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
job.id, job.status # "gen_…", "queued"
|
|
154
|
+
generation = job.wait(timeout=900, poll_interval=5)
|
|
155
|
+
print(generation.url) # alias for .output_url
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`wait()` polls `GET /generations/{id}` until a terminal status
|
|
159
|
+
(`succeeded`, `completed`, `failed`, `error`, `cancelled`, `canceled`).
|
|
160
|
+
It raises `JobFailedError` on a failed generation — pass
|
|
161
|
+
`raise_on_failure=False` to get the `Generation` back instead — and
|
|
162
|
+
`JobTimeoutError` if the timeout elapses first. `job.refresh()` polls once.
|
|
163
|
+
|
|
164
|
+
Anything left unset is omitted from the request body so the server applies its
|
|
165
|
+
own defaults (model `fal-ai/bytedance/seedance/v2`, duration 5, resolution
|
|
166
|
+
`720p`, sound on).
|
|
167
|
+
|
|
168
|
+
### Models, usage, account
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
for model in picx.models.list(type="video"):
|
|
172
|
+
print(model.id, model.name, model.credits)
|
|
173
|
+
|
|
174
|
+
usage = picx.account.usage(period=30)
|
|
175
|
+
print(usage.total_requests, usage.credits_used, usage.total_cost_usd, usage.model_breakdown)
|
|
176
|
+
|
|
177
|
+
me = picx.account.me()
|
|
178
|
+
print(me.email, me.is_active, me.credits)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`GET /models` is the only public endpoint, so `PicX(api_key=None).models.list()`
|
|
182
|
+
works without credentials.
|
|
183
|
+
|
|
184
|
+
Every result object also keeps the untouched response on `.raw`, so new API
|
|
185
|
+
fields are reachable before the SDK models them.
|
|
186
|
+
|
|
187
|
+
## Async
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
import asyncio, os
|
|
191
|
+
from picx import AsyncPicX
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def main() -> None:
|
|
195
|
+
async with AsyncPicX(os.environ["PICX_API_KEY"]) as picx:
|
|
196
|
+
job = await picx.video.create(prompt="sneaker on marble, slow orbit", duration=12)
|
|
197
|
+
asset = await job.wait()
|
|
198
|
+
print(asset.url)
|
|
199
|
+
|
|
200
|
+
image = await picx.images.generate("a red sneaker on white marble")
|
|
201
|
+
print(image.url)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
asyncio.run(main())
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The async surface mirrors the sync one method for method — only `await` and
|
|
208
|
+
`aclose()`/`async with` differ. A parity test enforces this.
|
|
209
|
+
|
|
210
|
+
## Error handling
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from picx import (
|
|
214
|
+
PicXError, # base class: .status_code, .request_id, .body, .message
|
|
215
|
+
ValidationError, # 400 / 422, and invalid arguments caught locally
|
|
216
|
+
AuthenticationError, # 401, or no API key configured
|
|
217
|
+
PermissionDeniedError, # 403, the key lacks the required scope
|
|
218
|
+
NotFoundError, # 404
|
|
219
|
+
RateLimitError, # 429, exposes .retry_after
|
|
220
|
+
ServerError, # 5xx
|
|
221
|
+
APIConnectionError, # DNS/TLS/socket failure
|
|
222
|
+
APITimeoutError, # subclass of APIConnectionError
|
|
223
|
+
JobFailedError, # a generation ended failed/cancelled
|
|
224
|
+
JobTimeoutError, # wait() timed out, generation still running
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
asset = picx.images.generate("a red sneaker")
|
|
229
|
+
except RateLimitError as exc:
|
|
230
|
+
print("slow down for", exc.retry_after, "seconds")
|
|
231
|
+
except PermissionDeniedError:
|
|
232
|
+
print("this key is missing the images:generate scope")
|
|
233
|
+
except PicXError as exc:
|
|
234
|
+
print(exc.status_code, exc.request_id, exc)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Errors are parsed from both response shapes the API uses — FastAPI's
|
|
238
|
+
`{"detail": …}` (including the 422 list form) and `{"error": …, "detail": …}`.
|
|
239
|
+
|
|
240
|
+
### Retries and idempotency
|
|
241
|
+
|
|
242
|
+
Retries apply to `429`, `5xx` and connection/timeout failures only — never to
|
|
243
|
+
other 4xx. Backoff is exponential with full jitter and honours `Retry-After`
|
|
244
|
+
(capped at 60s). `max_retries` defaults to 2; set `max_retries=0` to disable.
|
|
245
|
+
|
|
246
|
+
A `POST` is **not** replayed unless you supply an idempotency key, because
|
|
247
|
+
replaying it could charge twice:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
import uuid
|
|
251
|
+
|
|
252
|
+
asset = picx.images.generate("a red sneaker", idempotency_key=str(uuid.uuid4()))
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The key is sent as the `Idempotency-Key` header, which the backend honours on
|
|
256
|
+
calls that spend credits. `idempotency_key` is available on
|
|
257
|
+
`images.generate`, `images.edit` and `video.create`.
|
|
258
|
+
|
|
259
|
+
### API keys are never logged
|
|
260
|
+
|
|
261
|
+
The key is redacted from `repr()`/`str()` of the client and from every exception
|
|
262
|
+
message and response body (anything matching `pxsk_…` becomes
|
|
263
|
+
`pxsk_***REDACTED***`). Read it back deliberately with `picx.api_key` if you
|
|
264
|
+
really need it. This is covered by tests.
|
|
265
|
+
|
|
266
|
+
## Examples
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
export PICX_API_KEY=pxsk_...
|
|
270
|
+
python examples/generate_image.py "a red sneaker on white marble"
|
|
271
|
+
python examples/generate_video.py "sneaker on marble, slow orbit"
|
|
272
|
+
python examples/generate_video.py --async
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Development
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
uv venv --python 3.13
|
|
279
|
+
uv pip install -e ".[dev]"
|
|
280
|
+
uv run mypy
|
|
281
|
+
uv run pytest
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The test suite runs entirely against `httpx.MockTransport` — no network access,
|
|
285
|
+
no API key required.
|
|
286
|
+
|
|
287
|
+
## License
|
|
288
|
+
|
|
289
|
+
MIT
|
picx_ai-0.2.0/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# picx-ai
|
|
2
|
+
|
|
3
|
+
Official Python client for the [PicX](https://picxstudio.com) image and video generation API.
|
|
4
|
+
|
|
5
|
+
- Sync (`PicX`) and async (`AsyncPicX`) clients with the same surface
|
|
6
|
+
- Typed results and a typed exception hierarchy; ships `py.typed`, passes `mypy --strict`
|
|
7
|
+
- Automatic retries with exponential backoff + jitter, `Retry-After` aware
|
|
8
|
+
- `Idempotency-Key` support on the calls that spend credits
|
|
9
|
+
- One runtime dependency: [`httpx`](https://www.python-httpx.org/)
|
|
10
|
+
|
|
11
|
+
> **This is not the CLI.** This package is a library you install into your own
|
|
12
|
+
> application. The `admin-cli/` project in this organisation builds the internal
|
|
13
|
+
> `picx-admin` command line binary and is unrelated to this SDK.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install picx-ai
|
|
19
|
+
# or
|
|
20
|
+
uv add picx-ai
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Python 3.9+.
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import os
|
|
29
|
+
from picx import PicX
|
|
30
|
+
|
|
31
|
+
picx = PicX(os.environ["PICX_API_KEY"])
|
|
32
|
+
job = picx.video.create(prompt="sneaker on marble, slow orbit", duration=12)
|
|
33
|
+
asset = job.wait()
|
|
34
|
+
print(asset.url)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`PicX()` falls back to the `PICX_API_KEY` environment variable when no key is
|
|
38
|
+
passed, so `PicX()` works once the variable is set. Use it as a context manager
|
|
39
|
+
to close the connection pool deterministically:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
with PicX() as picx:
|
|
43
|
+
asset = picx.images.generate("a red sneaker on white marble", size="2K", aspect_ratio="1:1")
|
|
44
|
+
print(asset.url, asset.credits_used)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Configuration
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
picx = PicX(
|
|
51
|
+
api_key="pxsk_...", # defaults to os.environ["PICX_API_KEY"]
|
|
52
|
+
base_url="https://api.picxstudio.com/v1", # or the PICX_BASE_URL env var
|
|
53
|
+
timeout=60.0, # seconds, per request
|
|
54
|
+
max_retries=2, # retries for 429 / 5xx / connection errors
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
| Call | Endpoint | Notes |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `picx.images.generate(prompt, model=…, size=…, aspect_ratio=…)` | `POST /images/generate` | scope `images:generate`; `size` is `1K`/`2K`/`4K`, `aspect_ratio` like `16:9` |
|
|
63
|
+
| `picx.images.edit(instruction, image_urls, model=…, size=…)` | `POST /images/edit` | scope `images:edit`; 1-5 image URLs |
|
|
64
|
+
| `picx.video.create(prompt=…, duration=…, resolution=…, sound=…, …)` | `POST /videos/generate` | scope `videos:generate`; returns **202** and a job to poll |
|
|
65
|
+
| `picx.generations.get(id)` | `GET /generations/{id}` | raises `NotFoundError` on 404 |
|
|
66
|
+
| `picx.models.list(type="image")` | `GET /models` | **public**, no API key needed |
|
|
67
|
+
| `picx.account.usage(period=30)` | `GET /account/usage` | |
|
|
68
|
+
| `picx.account.me()` | `GET /account/me` | |
|
|
69
|
+
|
|
70
|
+
`picx.images` is also available as `picx.image`, and `picx.video` as `picx.videos`.
|
|
71
|
+
|
|
72
|
+
### Images
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
asset = picx.images.generate("a red sneaker on white marble", size="2K", aspect_ratio="1:1")
|
|
76
|
+
asset.id, asset.url, asset.model, asset.size, asset.aspect_ratio, asset.credits_used
|
|
77
|
+
|
|
78
|
+
edited = picx.images.edit("put it on a wet street at night", [asset.url], size="4K")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Videos (202 Accepted, then poll)
|
|
82
|
+
|
|
83
|
+
`POST /videos/generate` is asynchronous server-side, so `create()` returns a job:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
job = picx.video.create(
|
|
87
|
+
prompt="sneaker on marble, slow orbit",
|
|
88
|
+
duration=12, # server default 5
|
|
89
|
+
resolution="720p", # server default "720p"; must be priced for the model
|
|
90
|
+
sound=True, # server default True
|
|
91
|
+
aspect_ratio="16:9",
|
|
92
|
+
mode="image", # "text" | "image" | "reference"
|
|
93
|
+
image_url="https://…/ref.png",
|
|
94
|
+
callback_url="https://example.com/webhook",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
job.id, job.status # "gen_…", "queued"
|
|
98
|
+
generation = job.wait(timeout=900, poll_interval=5)
|
|
99
|
+
print(generation.url) # alias for .output_url
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`wait()` polls `GET /generations/{id}` until a terminal status
|
|
103
|
+
(`succeeded`, `completed`, `failed`, `error`, `cancelled`, `canceled`).
|
|
104
|
+
It raises `JobFailedError` on a failed generation — pass
|
|
105
|
+
`raise_on_failure=False` to get the `Generation` back instead — and
|
|
106
|
+
`JobTimeoutError` if the timeout elapses first. `job.refresh()` polls once.
|
|
107
|
+
|
|
108
|
+
Anything left unset is omitted from the request body so the server applies its
|
|
109
|
+
own defaults (model `fal-ai/bytedance/seedance/v2`, duration 5, resolution
|
|
110
|
+
`720p`, sound on).
|
|
111
|
+
|
|
112
|
+
### Models, usage, account
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
for model in picx.models.list(type="video"):
|
|
116
|
+
print(model.id, model.name, model.credits)
|
|
117
|
+
|
|
118
|
+
usage = picx.account.usage(period=30)
|
|
119
|
+
print(usage.total_requests, usage.credits_used, usage.total_cost_usd, usage.model_breakdown)
|
|
120
|
+
|
|
121
|
+
me = picx.account.me()
|
|
122
|
+
print(me.email, me.is_active, me.credits)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`GET /models` is the only public endpoint, so `PicX(api_key=None).models.list()`
|
|
126
|
+
works without credentials.
|
|
127
|
+
|
|
128
|
+
Every result object also keeps the untouched response on `.raw`, so new API
|
|
129
|
+
fields are reachable before the SDK models them.
|
|
130
|
+
|
|
131
|
+
## Async
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
import asyncio, os
|
|
135
|
+
from picx import AsyncPicX
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def main() -> None:
|
|
139
|
+
async with AsyncPicX(os.environ["PICX_API_KEY"]) as picx:
|
|
140
|
+
job = await picx.video.create(prompt="sneaker on marble, slow orbit", duration=12)
|
|
141
|
+
asset = await job.wait()
|
|
142
|
+
print(asset.url)
|
|
143
|
+
|
|
144
|
+
image = await picx.images.generate("a red sneaker on white marble")
|
|
145
|
+
print(image.url)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
asyncio.run(main())
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The async surface mirrors the sync one method for method — only `await` and
|
|
152
|
+
`aclose()`/`async with` differ. A parity test enforces this.
|
|
153
|
+
|
|
154
|
+
## Error handling
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from picx import (
|
|
158
|
+
PicXError, # base class: .status_code, .request_id, .body, .message
|
|
159
|
+
ValidationError, # 400 / 422, and invalid arguments caught locally
|
|
160
|
+
AuthenticationError, # 401, or no API key configured
|
|
161
|
+
PermissionDeniedError, # 403, the key lacks the required scope
|
|
162
|
+
NotFoundError, # 404
|
|
163
|
+
RateLimitError, # 429, exposes .retry_after
|
|
164
|
+
ServerError, # 5xx
|
|
165
|
+
APIConnectionError, # DNS/TLS/socket failure
|
|
166
|
+
APITimeoutError, # subclass of APIConnectionError
|
|
167
|
+
JobFailedError, # a generation ended failed/cancelled
|
|
168
|
+
JobTimeoutError, # wait() timed out, generation still running
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
asset = picx.images.generate("a red sneaker")
|
|
173
|
+
except RateLimitError as exc:
|
|
174
|
+
print("slow down for", exc.retry_after, "seconds")
|
|
175
|
+
except PermissionDeniedError:
|
|
176
|
+
print("this key is missing the images:generate scope")
|
|
177
|
+
except PicXError as exc:
|
|
178
|
+
print(exc.status_code, exc.request_id, exc)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Errors are parsed from both response shapes the API uses — FastAPI's
|
|
182
|
+
`{"detail": …}` (including the 422 list form) and `{"error": …, "detail": …}`.
|
|
183
|
+
|
|
184
|
+
### Retries and idempotency
|
|
185
|
+
|
|
186
|
+
Retries apply to `429`, `5xx` and connection/timeout failures only — never to
|
|
187
|
+
other 4xx. Backoff is exponential with full jitter and honours `Retry-After`
|
|
188
|
+
(capped at 60s). `max_retries` defaults to 2; set `max_retries=0` to disable.
|
|
189
|
+
|
|
190
|
+
A `POST` is **not** replayed unless you supply an idempotency key, because
|
|
191
|
+
replaying it could charge twice:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
import uuid
|
|
195
|
+
|
|
196
|
+
asset = picx.images.generate("a red sneaker", idempotency_key=str(uuid.uuid4()))
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The key is sent as the `Idempotency-Key` header, which the backend honours on
|
|
200
|
+
calls that spend credits. `idempotency_key` is available on
|
|
201
|
+
`images.generate`, `images.edit` and `video.create`.
|
|
202
|
+
|
|
203
|
+
### API keys are never logged
|
|
204
|
+
|
|
205
|
+
The key is redacted from `repr()`/`str()` of the client and from every exception
|
|
206
|
+
message and response body (anything matching `pxsk_…` becomes
|
|
207
|
+
`pxsk_***REDACTED***`). Read it back deliberately with `picx.api_key` if you
|
|
208
|
+
really need it. This is covered by tests.
|
|
209
|
+
|
|
210
|
+
## Examples
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
export PICX_API_KEY=pxsk_...
|
|
214
|
+
python examples/generate_image.py "a red sneaker on white marble"
|
|
215
|
+
python examples/generate_video.py "sneaker on marble, slow orbit"
|
|
216
|
+
python examples/generate_video.py --async
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Development
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
uv venv --python 3.13
|
|
223
|
+
uv pip install -e ".[dev]"
|
|
224
|
+
uv run mypy
|
|
225
|
+
uv run pytest
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The test suite runs entirely against `httpx.MockTransport` — no network access,
|
|
229
|
+
no API key required.
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Generate and then edit an image.
|
|
2
|
+
|
|
3
|
+
export PICX_API_KEY=pxsk_...
|
|
4
|
+
python examples/generate_image.py "a red sneaker on white marble"
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
from picx import (
|
|
14
|
+
AuthenticationError,
|
|
15
|
+
PermissionDeniedError,
|
|
16
|
+
PicX,
|
|
17
|
+
PicXError,
|
|
18
|
+
RateLimitError,
|
|
19
|
+
ValidationError,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> int:
|
|
24
|
+
if not os.environ.get("PICX_API_KEY"):
|
|
25
|
+
print("Set PICX_API_KEY first (get a key at https://picxstudio.com).", file=sys.stderr)
|
|
26
|
+
return 2
|
|
27
|
+
|
|
28
|
+
prompt = sys.argv[1] if len(sys.argv) > 1 else "a red sneaker on white marble, studio light"
|
|
29
|
+
|
|
30
|
+
# The API key is read from PICX_API_KEY when it is not passed explicitly.
|
|
31
|
+
with PicX() as picx:
|
|
32
|
+
image_models = [m for m in picx.models.list(type="image")]
|
|
33
|
+
if image_models:
|
|
34
|
+
print("Available image models:")
|
|
35
|
+
for model in image_models:
|
|
36
|
+
print(f" - {model.id} ({model.name}) - {model.credits} credits")
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
asset = picx.images.generate(
|
|
40
|
+
prompt,
|
|
41
|
+
size="2K",
|
|
42
|
+
aspect_ratio="1:1",
|
|
43
|
+
# An idempotency key makes this paid call safe to retry.
|
|
44
|
+
idempotency_key=str(uuid.uuid4()),
|
|
45
|
+
)
|
|
46
|
+
except ValidationError as exc:
|
|
47
|
+
print(f"The request was rejected: {exc}", file=sys.stderr)
|
|
48
|
+
return 1
|
|
49
|
+
except PermissionDeniedError:
|
|
50
|
+
print("This API key lacks the images:generate scope.", file=sys.stderr)
|
|
51
|
+
return 1
|
|
52
|
+
except AuthenticationError:
|
|
53
|
+
print("PICX_API_KEY is not a valid key.", file=sys.stderr)
|
|
54
|
+
return 1
|
|
55
|
+
except RateLimitError as exc:
|
|
56
|
+
print(f"Rate limited; retry in {exc.retry_after or 'a few'} seconds.", file=sys.stderr)
|
|
57
|
+
return 1
|
|
58
|
+
except PicXError as exc:
|
|
59
|
+
print(f"PicX error: {exc}", file=sys.stderr)
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
print(f"\nGenerated {asset.id}: {asset.url}")
|
|
63
|
+
print(f"model={asset.model} size={asset.size} credits_used={asset.credits_used}")
|
|
64
|
+
|
|
65
|
+
edited = picx.images.edit(
|
|
66
|
+
"put the sneaker on a wet street at night",
|
|
67
|
+
[asset.url],
|
|
68
|
+
idempotency_key=str(uuid.uuid4()),
|
|
69
|
+
)
|
|
70
|
+
print(f"Edited {edited.id}: {edited.url} (credits_used={edited.credits_used})")
|
|
71
|
+
|
|
72
|
+
usage = picx.account.usage(period=7)
|
|
73
|
+
print(
|
|
74
|
+
f"\nLast {usage.period_days} days: {usage.total_requests} requests, "
|
|
75
|
+
f"{usage.credits_used} credits, ${usage.total_cost_usd:.2f}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
raise SystemExit(main())
|