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.
@@ -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
@@ -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())