imagestep 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ .venv/
5
+ __pycache__/
6
+ *.pyc
7
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jun Zhang
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.
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.5
2
+ Name: imagestep
3
+ Version: 0.1.0
4
+ Summary: ImageStep SDK — the image step for your automations
5
+ Project-URL: Homepage, https://imagestep.dev
6
+ Project-URL: Documentation, https://imagestep.dev/docs
7
+ Project-URL: Repository, https://github.com/jun-zhang-pro/imagestep
8
+ Project-URL: Issues, https://imagestep.dev/support
9
+ Author: ImageStep
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ License-File: THIRD-PARTY-NOTICES.md
13
+ Keywords: api,automation,image,image-generation,imagestep,mcp,n8n,remove-background,upscale
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Multimedia :: Graphics
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Requires-Dist: httpx>=0.27
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
32
+ Requires-Dist: pytest>=8; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # imagestep
36
+
37
+ The image step for your automations — one API key, one job handle, one stable URL.
38
+ Generate, edit, remove backgrounds, upscale, convert and read metadata from Python 3.10+.
39
+ One runtime dependency (`httpx`), a sync and an async client, typed (TypedDicts generated from
40
+ the API's OpenAPI document).
41
+
42
+ ```python
43
+ import os
44
+ from imagestep import ImageStep
45
+
46
+ client = ImageStep(api_key=os.environ["IMAGESTEP_API_KEY"])
47
+
48
+ asset = client.assets.upload("./product.jpg") # stage → PUT → finish → ready
49
+ job = client.ops.remove_bg(asset["id"], wait=True) # or any op: upscale, resize, convert, generate…
50
+ [cutout] = client.jobs.outputs(job)
51
+ [published] = client.assets.publish(cutout["id"])
52
+ print(published["publicUrl"]) # https://cdn.imagestep.dev/<id>
53
+ ```
54
+
55
+ **Only want the image back?** One call, nothing stored:
56
+
57
+ ```python
58
+ small = client.images.transform("resize", file="./product.jpg", parameters={"width": 1200})
59
+ open("out.jpg", "wb").write(small)
60
+
61
+ # One op does one thing. Resize AND re-encode is two steps — save them as a preset and run it in one call:
62
+ webp = client.images.transform(None, file="./product.jpg", preset="web-optimize")
63
+ # your own render template, then a PNG from it (pin a version with f"{card['id']}@1")
64
+ card = client.templates.create({"name": "price-card", "html": "<h1>{{ title }}</h1>", "width": 1200, "height": 630})
65
+ png = client.images.render(card["id"], {"title": "Hello"})
66
+ ```
67
+
68
+ Two paths, and the line between them is not speed — it is **who carries the retry**. `ops.*` gives
69
+ you a job: this service promises to finish it, which is what buys progress, cancellation, webhooks
70
+ and an `asset_id`. `images.*` runs while you wait and stores nothing, because you are still holding
71
+ the input, so a failure costs you one re-send. AI ops and batches are always jobs.
72
+
73
+ ## Install
74
+
75
+ ```sh
76
+ pip install imagestep
77
+ ```
78
+
79
+ `ImageStep()` with no arguments reads `IMAGESTEP_API_KEY` (and `IMAGESTEP_BASE_URL`, default
80
+ `https://api.imagestep.dev`). `AsyncImageStep` has the identical surface with every method
81
+ awaitable — `async with AsyncImageStep() as client: await client.ops.upscale(...)`, and that
82
+ includes `images.*` (`await client.images.transform("resize", file=…, parameters={"width": 1200})`).
83
+
84
+ `construct_webhook_event` raises `WebhookSignatureError` when the signature does not check out; the
85
+ JavaScript SDK throws a plain `Error` there, and takes its key as an argument rather than from the
86
+ environment (it also runs on edge runtimes that have no `process.env`).
87
+
88
+ ## What you get
89
+
90
+ | | |
91
+ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
92
+ | `client.ops.run(op, variants=[...])` | one call, one asset per variant — the whole set of social sizes (contract §8) |
93
+ | `client.ops` | `list` `get` `run` `estimate` `remove_bg` `upscale` `restore_face` `colorize` `analyze` `generate` `edit` `resize` `convert` `compress` `crop` `pad` `grayscale` `rotate` `flip` `flop` `trim` `flatten` `adjust` `mask` `blur_region` `overlay` `caption` `read_metadata` — the atomic-op vocabulary: `run(op, **opts)` submits any op as a job, `estimate` prices one without creating anything, and the rest are one helper per op |
94
+ | `client.images` | `sync_endpoints` `supports` `transform` `transform_result` `render` `metadata` — the synchronous face (contract §9): `transform(op, file=… \| url=… \| asset_id=…, parameters={…})` → `bytes`, `transform_result(…)` → a `BinaryResult` with `content_type` / `width` / `height` as well, `render(template_id, data)` → PNG, `metadata(file)` → dict. Which ops may go this way is `GET /api/v1/ops`, never a list in this package |
95
+ | `client.assets` | `upload` `upload_many` `from_url` `wait_ready` `status` `download` `get` `list` `iterate` `collections` `iterate_collections` `rename_collection` `publish` `unpublish` `set_collection` `tag` `delete` — `upload()` takes a path · bytes · a binary file object, dedupes by sha1 and waits for ingest; `upload_many()` does many at once (one stage and one finish call per 500, `concurrency` PUTs, one status call per tick); `from_url()` has the SERVICE fetch each link instead, 20 to a request |
96
+ | `client.jobs` | `submit` `estimate` `get` `items` `iterate_items` `list` `iterate` `cancel` `resume` `wait` `outputs` — `wait(job_id, on_progress=…)` waits for completion (the service holds each read open — `GET /jobs/{id}?wait=` — so a five-second job costs one request, not a poll loop) and `outputs(job)` reads what it produced as list rows — one paged `GET /assets?job_id=`, not one read per item (#441) — or subscribe to `job.completed` webhooks instead of polling. The totals by status and type have no method — `client.get("/api/v1/jobs/counts")`; for one status, `list(status=…, per_page=1)` and read `meta["total"]` |
97
+ | `client.presets` | `list` `get` `create` `update` `delete` `delete_version` `import_` `run` — versioned lists of steps you save once and run by slug: `presets.run(slug, asset_ids, wait=True)` (`"slug@3"` pins version 3); `presets.import_()` (keyword clash); `presets.create({"name": …, "steps": [{"op": "generate", "prompt": "{{subject.hero}} on a rooftop"}], "subjects": [{"name": "hero", "referenceAssetIds": [asset_id], "descriptor": "a matte black bottle…"}]})` — the images pin the geometry (max 4 across all subjects, sent with the preset's `generate` / `edit` step) and the descriptor pins the words, expanding into the prompt wherever you write `{{subject.hero}}`; `get(slug)` exports `steps` + `subjects` + `version` + `versions`. A preset whose steps mix a model with other steps runs as **one** `chain` job, the image in between handed on for you; `jobs.estimate` prices it per segment in `steps` A preset keeps 50 versions on record: at the ceiling `update` is `422 resource_limit_exceeded` rather than dropping the oldest, and `delete_version(slug, n)` makes room — `slug@n` answers 404 from then on, so it is for versions nothing pins. |
98
+ | `client.templates` | `list` `iterate` `get` `versions` `create` `update` `delete` `import_` — HTML/CSS render templates (#24 / #234); `list` is one page of rows without html / css (#497), `get` the whole document; versioned: `update` saves `version + 1`, and `id@version` reads or renders one frozen version; a batch is a job: `ops.run("render_template", template_id=…, items=[…])` |
99
+ | `client.models` | `list` — the model catalogue with prices; `list("ai_image")` (default) or `list("analyze")` |
100
+ | `client.webhooks` | `list` `get` `create` `update` `delete` `rotate_secret` `test` `deliveries` `iterate_deliveries` `verify` `construct_event` — `create()` answers the signing secret once; `verify(raw_body, header, secret)` and `construct_event(…)` check a delivery's signature in your own handler |
101
+ | `client.agent` | `guidelines` `feedback` `reports` `iterate_reports` — what this API expects of an agent, and the channel for telling us an op you needed is missing |
102
+ | `client.usage` | `get` — credits charged, jobs created and items settled over a window, grouped by `op`, `key` or `day` (contract §11) |
103
+ | `ImageStepError` | every failure the API answers: `code` (closed set), `retryable` (what to branch on — without the service's own, true for a 429 or a 5xx), `param`, `details`, `retry_after`, and `request_id` — `error.requestId`, else the `X-Request-Id` header; quote it when reporting a failure (contract §11). Writes carry an `Idempotency-Key` per call, reused across the SDK's own retries; pass `idempotency_key` to make your own retry the same submission |
104
+
105
+ ## Pagination
106
+
107
+ Every list takes `page` (from 0) and `per_page` (100 by default and at most) and answers a `Page` —
108
+ `items` plus `meta` (`total`, `page`, `perPage`, `hasMore`, `nextCursor`). Out of range is clamped, not
109
+ refused, and `meta` reports the page actually served. Pass `cursor=meta["nextCursor"]` instead of a page
110
+ number to read the rows after it: the service counts nothing then (no `total`, no `page`), and page 1 000
111
+ costs what page 1 did.
112
+
113
+ Don't write the loop — each listing has an iterator that walks to the end, following the `nextCursor` each
114
+ *answer* carries:
115
+
116
+ ```python
117
+ for asset in client.assets.iterate(collection="shoot-01"):
118
+ print(asset["id"])
119
+ ```
120
+
121
+ `assets.iterate` · `assets.iterate_collections` · `jobs.iterate` · `jobs.iterate_items(id)` · `templates.iterate` ·
122
+ `webhooks.iterate_deliveries(id)` · `agent.iterate_reports`, on both clients (`async for` on `AsyncImageStep`).
123
+ Pass `cursor=` (or `page=`) to resume a walk.
124
+
125
+ Every signature, parameter and return shape: **<https://imagestep.dev/docs/sdk>** — one reference for
126
+ both SDKs, held against these sources by a test (imagestep#297).
@@ -0,0 +1,92 @@
1
+ # imagestep
2
+
3
+ The image step for your automations — one API key, one job handle, one stable URL.
4
+ Generate, edit, remove backgrounds, upscale, convert and read metadata from Python 3.10+.
5
+ One runtime dependency (`httpx`), a sync and an async client, typed (TypedDicts generated from
6
+ the API's OpenAPI document).
7
+
8
+ ```python
9
+ import os
10
+ from imagestep import ImageStep
11
+
12
+ client = ImageStep(api_key=os.environ["IMAGESTEP_API_KEY"])
13
+
14
+ asset = client.assets.upload("./product.jpg") # stage → PUT → finish → ready
15
+ job = client.ops.remove_bg(asset["id"], wait=True) # or any op: upscale, resize, convert, generate…
16
+ [cutout] = client.jobs.outputs(job)
17
+ [published] = client.assets.publish(cutout["id"])
18
+ print(published["publicUrl"]) # https://cdn.imagestep.dev/<id>
19
+ ```
20
+
21
+ **Only want the image back?** One call, nothing stored:
22
+
23
+ ```python
24
+ small = client.images.transform("resize", file="./product.jpg", parameters={"width": 1200})
25
+ open("out.jpg", "wb").write(small)
26
+
27
+ # One op does one thing. Resize AND re-encode is two steps — save them as a preset and run it in one call:
28
+ webp = client.images.transform(None, file="./product.jpg", preset="web-optimize")
29
+ # your own render template, then a PNG from it (pin a version with f"{card['id']}@1")
30
+ card = client.templates.create({"name": "price-card", "html": "<h1>{{ title }}</h1>", "width": 1200, "height": 630})
31
+ png = client.images.render(card["id"], {"title": "Hello"})
32
+ ```
33
+
34
+ Two paths, and the line between them is not speed — it is **who carries the retry**. `ops.*` gives
35
+ you a job: this service promises to finish it, which is what buys progress, cancellation, webhooks
36
+ and an `asset_id`. `images.*` runs while you wait and stores nothing, because you are still holding
37
+ the input, so a failure costs you one re-send. AI ops and batches are always jobs.
38
+
39
+ ## Install
40
+
41
+ ```sh
42
+ pip install imagestep
43
+ ```
44
+
45
+ `ImageStep()` with no arguments reads `IMAGESTEP_API_KEY` (and `IMAGESTEP_BASE_URL`, default
46
+ `https://api.imagestep.dev`). `AsyncImageStep` has the identical surface with every method
47
+ awaitable — `async with AsyncImageStep() as client: await client.ops.upscale(...)`, and that
48
+ includes `images.*` (`await client.images.transform("resize", file=…, parameters={"width": 1200})`).
49
+
50
+ `construct_webhook_event` raises `WebhookSignatureError` when the signature does not check out; the
51
+ JavaScript SDK throws a plain `Error` there, and takes its key as an argument rather than from the
52
+ environment (it also runs on edge runtimes that have no `process.env`).
53
+
54
+ ## What you get
55
+
56
+ | | |
57
+ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58
+ | `client.ops.run(op, variants=[...])` | one call, one asset per variant — the whole set of social sizes (contract §8) |
59
+ | `client.ops` | `list` `get` `run` `estimate` `remove_bg` `upscale` `restore_face` `colorize` `analyze` `generate` `edit` `resize` `convert` `compress` `crop` `pad` `grayscale` `rotate` `flip` `flop` `trim` `flatten` `adjust` `mask` `blur_region` `overlay` `caption` `read_metadata` — the atomic-op vocabulary: `run(op, **opts)` submits any op as a job, `estimate` prices one without creating anything, and the rest are one helper per op |
60
+ | `client.images` | `sync_endpoints` `supports` `transform` `transform_result` `render` `metadata` — the synchronous face (contract §9): `transform(op, file=… \| url=… \| asset_id=…, parameters={…})` → `bytes`, `transform_result(…)` → a `BinaryResult` with `content_type` / `width` / `height` as well, `render(template_id, data)` → PNG, `metadata(file)` → dict. Which ops may go this way is `GET /api/v1/ops`, never a list in this package |
61
+ | `client.assets` | `upload` `upload_many` `from_url` `wait_ready` `status` `download` `get` `list` `iterate` `collections` `iterate_collections` `rename_collection` `publish` `unpublish` `set_collection` `tag` `delete` — `upload()` takes a path · bytes · a binary file object, dedupes by sha1 and waits for ingest; `upload_many()` does many at once (one stage and one finish call per 500, `concurrency` PUTs, one status call per tick); `from_url()` has the SERVICE fetch each link instead, 20 to a request |
62
+ | `client.jobs` | `submit` `estimate` `get` `items` `iterate_items` `list` `iterate` `cancel` `resume` `wait` `outputs` — `wait(job_id, on_progress=…)` waits for completion (the service holds each read open — `GET /jobs/{id}?wait=` — so a five-second job costs one request, not a poll loop) and `outputs(job)` reads what it produced as list rows — one paged `GET /assets?job_id=`, not one read per item (#441) — or subscribe to `job.completed` webhooks instead of polling. The totals by status and type have no method — `client.get("/api/v1/jobs/counts")`; for one status, `list(status=…, per_page=1)` and read `meta["total"]` |
63
+ | `client.presets` | `list` `get` `create` `update` `delete` `delete_version` `import_` `run` — versioned lists of steps you save once and run by slug: `presets.run(slug, asset_ids, wait=True)` (`"slug@3"` pins version 3); `presets.import_()` (keyword clash); `presets.create({"name": …, "steps": [{"op": "generate", "prompt": "{{subject.hero}} on a rooftop"}], "subjects": [{"name": "hero", "referenceAssetIds": [asset_id], "descriptor": "a matte black bottle…"}]})` — the images pin the geometry (max 4 across all subjects, sent with the preset's `generate` / `edit` step) and the descriptor pins the words, expanding into the prompt wherever you write `{{subject.hero}}`; `get(slug)` exports `steps` + `subjects` + `version` + `versions`. A preset whose steps mix a model with other steps runs as **one** `chain` job, the image in between handed on for you; `jobs.estimate` prices it per segment in `steps` A preset keeps 50 versions on record: at the ceiling `update` is `422 resource_limit_exceeded` rather than dropping the oldest, and `delete_version(slug, n)` makes room — `slug@n` answers 404 from then on, so it is for versions nothing pins. |
64
+ | `client.templates` | `list` `iterate` `get` `versions` `create` `update` `delete` `import_` — HTML/CSS render templates (#24 / #234); `list` is one page of rows without html / css (#497), `get` the whole document; versioned: `update` saves `version + 1`, and `id@version` reads or renders one frozen version; a batch is a job: `ops.run("render_template", template_id=…, items=[…])` |
65
+ | `client.models` | `list` — the model catalogue with prices; `list("ai_image")` (default) or `list("analyze")` |
66
+ | `client.webhooks` | `list` `get` `create` `update` `delete` `rotate_secret` `test` `deliveries` `iterate_deliveries` `verify` `construct_event` — `create()` answers the signing secret once; `verify(raw_body, header, secret)` and `construct_event(…)` check a delivery's signature in your own handler |
67
+ | `client.agent` | `guidelines` `feedback` `reports` `iterate_reports` — what this API expects of an agent, and the channel for telling us an op you needed is missing |
68
+ | `client.usage` | `get` — credits charged, jobs created and items settled over a window, grouped by `op`, `key` or `day` (contract §11) |
69
+ | `ImageStepError` | every failure the API answers: `code` (closed set), `retryable` (what to branch on — without the service's own, true for a 429 or a 5xx), `param`, `details`, `retry_after`, and `request_id` — `error.requestId`, else the `X-Request-Id` header; quote it when reporting a failure (contract §11). Writes carry an `Idempotency-Key` per call, reused across the SDK's own retries; pass `idempotency_key` to make your own retry the same submission |
70
+
71
+ ## Pagination
72
+
73
+ Every list takes `page` (from 0) and `per_page` (100 by default and at most) and answers a `Page` —
74
+ `items` plus `meta` (`total`, `page`, `perPage`, `hasMore`, `nextCursor`). Out of range is clamped, not
75
+ refused, and `meta` reports the page actually served. Pass `cursor=meta["nextCursor"]` instead of a page
76
+ number to read the rows after it: the service counts nothing then (no `total`, no `page`), and page 1 000
77
+ costs what page 1 did.
78
+
79
+ Don't write the loop — each listing has an iterator that walks to the end, following the `nextCursor` each
80
+ *answer* carries:
81
+
82
+ ```python
83
+ for asset in client.assets.iterate(collection="shoot-01"):
84
+ print(asset["id"])
85
+ ```
86
+
87
+ `assets.iterate` · `assets.iterate_collections` · `jobs.iterate` · `jobs.iterate_items(id)` · `templates.iterate` ·
88
+ `webhooks.iterate_deliveries(id)` · `agent.iterate_reports`, on both clients (`async for` on `AsyncImageStep`).
89
+ Pass `cursor=` (or `page=`) to resume a walk.
90
+
91
+ Every signature, parameter and return shape: **<https://imagestep.dev/docs/sdk>** — one reference for
92
+ both SDKs, held against these sources by a test (imagestep#297).