storeshot 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.
Files changed (45) hide show
  1. storeshot-0.1.0/.gitignore +8 -0
  2. storeshot-0.1.0/CHANGELOG.md +7 -0
  3. storeshot-0.1.0/LICENSE +202 -0
  4. storeshot-0.1.0/NOTICE +4 -0
  5. storeshot-0.1.0/PKG-INFO +382 -0
  6. storeshot-0.1.0/README.md +352 -0
  7. storeshot-0.1.0/pyproject.toml +89 -0
  8. storeshot-0.1.0/src/storeshot/__init__.py +41 -0
  9. storeshot-0.1.0/src/storeshot/_client.py +240 -0
  10. storeshot-0.1.0/src/storeshot/_compat.py +19 -0
  11. storeshot-0.1.0/src/storeshot/_core/__init__.py +1 -0
  12. storeshot-0.1.0/src/storeshot/_core/clock.py +26 -0
  13. storeshot-0.1.0/src/storeshot/_core/headers.py +67 -0
  14. storeshot-0.1.0/src/storeshot/_core/operation.py +23 -0
  15. storeshot-0.1.0/src/storeshot/_core/options.py +31 -0
  16. storeshot-0.1.0/src/storeshot/_core/redact.py +38 -0
  17. storeshot-0.1.0/src/storeshot/_core/request.py +316 -0
  18. storeshot-0.1.0/src/storeshot/_core/retry.py +99 -0
  19. storeshot-0.1.0/src/storeshot/_errors.py +153 -0
  20. storeshot-0.1.0/src/storeshot/_generated/__init__.py +1 -0
  21. storeshot-0.1.0/src/storeshot/_generated/operations.py +833 -0
  22. storeshot-0.1.0/src/storeshot/_generated/schemas.py +1981 -0
  23. storeshot-0.1.0/src/storeshot/_helpers/__init__.py +1 -0
  24. storeshot-0.1.0/src/storeshot/_helpers/archive.py +61 -0
  25. storeshot-0.1.0/src/storeshot/_helpers/files.py +49 -0
  26. storeshot-0.1.0/src/storeshot/_helpers/pagination.py +29 -0
  27. storeshot-0.1.0/src/storeshot/_helpers/wait.py +82 -0
  28. storeshot-0.1.0/src/storeshot/_operation_map.py +91 -0
  29. storeshot-0.1.0/src/storeshot/_resources/__init__.py +1 -0
  30. storeshot-0.1.0/src/storeshot/_resources/activity.py +30 -0
  31. storeshot-0.1.0/src/storeshot/_resources/api_keys.py +29 -0
  32. storeshot-0.1.0/src/storeshot/_resources/auth.py +187 -0
  33. storeshot-0.1.0/src/storeshot/_resources/billing.py +102 -0
  34. storeshot-0.1.0/src/storeshot/_resources/catalog.py +43 -0
  35. storeshot-0.1.0/src/storeshot/_resources/generations.py +88 -0
  36. storeshot-0.1.0/src/storeshot/_resources/palettes.py +19 -0
  37. storeshot-0.1.0/src/storeshot/_resources/projects.py +72 -0
  38. storeshot-0.1.0/src/storeshot/_resources/screenshot_sets.py +160 -0
  39. storeshot-0.1.0/src/storeshot/_resources/uploads.py +76 -0
  40. storeshot-0.1.0/src/storeshot/_resources/usage.py +30 -0
  41. storeshot-0.1.0/src/storeshot/_resources/webhooks.py +101 -0
  42. storeshot-0.1.0/src/storeshot/_version.py +3 -0
  43. storeshot-0.1.0/src/storeshot/py.typed +0 -0
  44. storeshot-0.1.0/src/storeshot/types.py +32 -0
  45. storeshot-0.1.0/src/storeshot/webhooks.py +95 -0
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ .tmp/
3
+ dist/
4
+ __pycache__/
5
+ *.egg-info/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ - First release: a typed, synchronous client for every operation of the Storeshot API contract
6
+ 1.17.0, with waits, uploads, archives, pagination, safe retries, idempotency keys, device
7
+ authorization and webhook signature verification.
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
storeshot-0.1.0/NOTICE ADDED
@@ -0,0 +1,4 @@
1
+ Storeshot Python SDK (storeshot)
2
+ Copyright 2026 Draggable
3
+
4
+ This product is licensed under the Apache License, Version 2.0 (see LICENSE).
@@ -0,0 +1,382 @@
1
+ Metadata-Version: 2.5
2
+ Name: storeshot
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Storeshot API: App Store and Google Play marketing screenshots from raw app screenshots.
5
+ Project-URL: Homepage, https://storeshot.art/docs/
6
+ Project-URL: Source, https://github.com/Draggable/storeshot-python
7
+ Project-URL: Changelog, https://github.com/Draggable/storeshot-python/blob/main/CHANGELOG.md
8
+ Author-email: Draggable <support@storeshot.art>
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ License-File: NOTICE
12
+ Keywords: api,app store,google play,screenshots,sdk,storeshot
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: 3.15
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: httpx<1,>=0.27
28
+ Requires-Dist: typing-extensions>=4.7; python_version < '3.11'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # storeshot
32
+
33
+ The official Python SDK for the [Storeshot API](https://storeshot.art/docs/): turn raw app
34
+ screenshots into App Store and Google Play marketing images. Typed from the API contract (OpenAPI),
35
+ one runtime dependency (httpx), Python 3.10 or later.
36
+
37
+ > This repository is exported from Storeshot's private monorepo, where the SDK is developed and
38
+ > tested; changes land there and arrive here with each release. Issues are welcome here or at
39
+ > support@storeshot.art.
40
+
41
+ ## Install
42
+
43
+ ```sh
44
+ pip install storeshot
45
+ # or
46
+ uv add storeshot
47
+ ```
48
+
49
+ ## API key
50
+
51
+ The client reads the key from the `STORESHOT_API_KEY` environment variable (or `Storeshot(api_key)`).
52
+ Create one in the developer console (https://console.storeshot.art/):
53
+
54
+ - `ss_test_…` keys are free: everything works, but the images are sample images and no credits
55
+ are used. Start with one.
56
+ - `ss_live_…` keys make real images and spend credits (1 per image).
57
+
58
+ Never print, log or commit a key, and never paste it into a chat. The SDK never logs the key and
59
+ masks it in `repr()`, `str()` and error messages. `Storeshot(environment="test")` refuses any key
60
+ that is not a test key before anything is sent, so a test suite cannot spend credits by accident.
61
+
62
+ No key yet? A tool can get one with its human's approval (device authorization, RFC 8628). A
63
+ client created with `Storeshot(None)` can call only `client.auth.*`; `link_account` shows your
64
+ human a console link and a code through `on_code`, waits for the approval and returns the new key
65
+ once:
66
+
67
+ ```python
68
+ from storeshot import Storeshot
69
+
70
+ linked = Storeshot(None).auth.link_account(
71
+ client_name="my-tool",
72
+ on_code=lambda code: print(f"Open {code.verification_uri_complete}, confirm {code.user_code}."),
73
+ )
74
+ # Store linked.api_key in your secret store (never print it), then:
75
+ client = Storeshot(linked.api_key)
76
+ ```
77
+
78
+ It raises `StoreshotError` `DEVICE_ACCESS_DENIED` or `DEVICE_CODE_EXPIRED` when the human denies
79
+ the request or does not answer within 10 minutes.
80
+
81
+ ## Quick start
82
+
83
+ From one raw screenshot (`home.png`) to an App Store listing in fastlane's folder layout:
84
+
85
+ <!-- sdk-readme-example -->
86
+
87
+ ```python
88
+ from storeshot import Storeshot
89
+
90
+ storeshot = Storeshot() # reads STORESHOT_API_KEY
91
+ upload = storeshot.uploads.create(file="home.png")
92
+ created = storeshot.screenshot_sets.create(
93
+ {
94
+ "app": {"name": "Trailmate"},
95
+ "screens": [{"input": {"uploadId": upload["id"]}, "caption": "Find every trail"}],
96
+ "stores": [{"store": "app_store"}],
97
+ }
98
+ )
99
+ done = storeshot.wait_for_screenshot_set(created)
100
+ print(done["status"], f"{done['counts']['succeeded']}/{done['images']} images")
101
+ storeshot.screenshot_sets.download_archive(done["id"], "listing.zip", layout="fastlane")
102
+ ```
103
+
104
+ Unzip `listing.zip` in your project root: fastlane `deliver` reads `fastlane/screenshots/`. With a
105
+ live key, ask for the price first: `storeshot.screenshot_sets.quote(body)` returns `images`,
106
+ `credits` and `sufficient` and creates nothing. For a single image use
107
+ `storeshot.generations.create({"inputs": [{"uploadId": upload["id"]}], "store": "app_store",
108
+ "device": "app_store_1290x2796", "app": {"name": "Trailmate"}})`; `storeshot.devices.list()`
109
+ lists the sizes.
110
+
111
+ ## Typing
112
+
113
+ Requests and answers are plain dicts whose keys are the API's own names (`uploadId`, `nextCursor`),
114
+ typed as `TypedDict`s from `storeshot.types` (`Generation`, `ScreenshotSet`,
115
+ `CreateScreenshotSetRequest`, …): your editor completes the keys and mypy or pyright check them.
116
+ They match the [API reference](https://storeshot.art/docs/api/) one to one. Only SDK options and
117
+ values the SDK builds itself (`LinkedAccount`, `ResponseMeta`, `DownloadedArchive`) use snake_case.
118
+
119
+ ## Waiting
120
+
121
+ Generations and screenshot sets are asynchronous: a create returns `queued` at once.
122
+
123
+ ```python
124
+ generation = storeshot.wait_for_generation(created_generation) # or its id
125
+ if generation["status"] == "succeeded":
126
+ print(generation["result"]["image"]["url"])
127
+
128
+ finished = storeshot.wait_for_screenshot_set(set_id, timeout=15 * 60)
129
+ ```
130
+
131
+ - `wait_for_generation` long-polls with `Prefer: wait` (the server holds each request up to 60 s).
132
+ - `wait_for_screenshot_set` polls every 3 s (`poll_interval`).
133
+ - Both return the final resource, also when it `failed`, was `canceled` or (sets)
134
+ `partially_failed`: check `status`. They raise `StoreshotTimeoutError` (`WAIT_TIMEOUT`) after
135
+ `timeout` seconds (default 600).
136
+ - Prefer webhooks (below) for long-running work in servers.
137
+
138
+ ## Uploads
139
+
140
+ ```python
141
+ storeshot.uploads.create(file="screens/home.png") # a path (str or pathlib.Path)
142
+ storeshot.uploads.create(file=data, filename="home.png") # bytes, bytearray or memoryview
143
+ with open("home.png", "rb") as handle:
144
+ storeshot.uploads.create(file=handle) # a binary file object
145
+ storeshot.uploads.create(url="https://example.com/home.png") # the server fetches it
146
+ ```
147
+
148
+ PNG, JPEG or WebP, at most 20 MiB (checked before anything is sent) and 50 megapixels. Uploads are
149
+ deleted after 30 days. Reference an upload by `id` in generations and sets of the same key
150
+ environment (test or live).
151
+
152
+ ## Downloads
153
+
154
+ ```python
155
+ data = storeshot.screenshot_sets.archive(set_id) # bytes
156
+ with storeshot.screenshot_sets.archive_stream(set_id) as stream:
157
+ for chunk in stream.iter_bytes():
158
+ ...
159
+ storeshot.screenshot_sets.download_archive(set_id, "listing.zip", layout="fastlane")
160
+ ```
161
+
162
+ `download_archive` writes `listing.zip.part` and renames it when complete, so a failed download
163
+ never leaves a partial file. Image URLs in `generation["result"]` are signed for 15 minutes: get
164
+ the generation again for fresh ones.
165
+
166
+ ## Pagination
167
+
168
+ ```python
169
+ page = storeshot.generations.list({"status": "succeeded", "limit": 50}) # one page
170
+ for generation in storeshot.generations.list_all({"status": "succeeded", "limit": 50}):
171
+ print(generation["id"]) # every page, fetched lazily
172
+ ```
173
+
174
+ `list()` returns one page (`data`, `nextCursor`); `list_all()` follows the cursors on demand, so
175
+ `itertools.islice` stops early. Uploads, generations, screenshot sets, projects, activity and
176
+ webhook deliveries (`webhooks.list_all_deliveries(id)`) have it.
177
+
178
+ ## Errors
179
+
180
+ Every failure is a `StoreshotError` (subclasses `StoreshotConnectionError`,
181
+ `StoreshotTimeoutError`, `StoreshotWebhookError`):
182
+
183
+ ```python
184
+ from storeshot import StoreshotError
185
+
186
+ try:
187
+ storeshot.screenshot_sets.create(body)
188
+ except StoreshotError as error:
189
+ if error.code == "INSUFFICIENT_CREDITS":
190
+ print(f"Not enough credits: top up at {(error.details or {}).get('topUpUrl')}")
191
+ else:
192
+ raise
193
+ ```
194
+
195
+ | Attribute | Meaning |
196
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
197
+ | `status` | HTTP status (0 when there was no response) |
198
+ | `code` | API error code (`ERROR_CODES`, e.g. `INVALID_INPUT`, `RATE_LIMITED`) or an SDK code (`CONNECTION_ERROR`, `TIMEOUT`, `WAIT_TIMEOUT`, …) |
199
+ | `message` | Human-readable, safe to show |
200
+ | `request_id` | Quote it when asking support@storeshot.art for help |
201
+ | `details` | Code-specific data (`topUpUrl`, `requiredCredits`, `issues`, …) |
202
+ | `doc_url` | Documentation of the code |
203
+ | `retry_after_seconds` | From `Retry-After` (429 and busy 503 answers) |
204
+
205
+ ## Retries and timeouts
206
+
207
+ - `timeout` (default 60 s) bounds each network operation of an attempt (connecting, every read,
208
+ every write); archive downloads default to 300 s.
209
+ - `max_retries` (default 2) retries with capped exponential backoff and jitter:
210
+ - `429` and busy `503` answers with `Retry-After`, for every request: the server refused them
211
+ before doing anything. A `Retry-After` longer than `max_retry_after` (default 60 s, e.g. the
212
+ daily generation cap) is not waited for: you get the error with `retry_after_seconds`.
213
+ - Other `5xx`, network errors and timeouts only where repeating is safe: `GET`, creates sent
214
+ with an `Idempotency-Key`, quotes and palettes. Other writes (uploads, webhooks, projects,
215
+ deletes, …) are never repeated blindly; the error says the request may have been processed.
216
+ - Per call: `timeout`, `max_retries`, `request_id`, `headers` and `on_response`
217
+ (`storeshot.me(request_id="build-42")`).
218
+ - `on_response` (client option or per call) receives a `ResponseMeta` with `request_id`,
219
+ `rate_limit`, `rate_limit_policy`, `idempotent_replayed` and the headers of every response.
220
+
221
+ ## Idempotency
222
+
223
+ `generations.create`, `screenshot_sets.create`, `billing.create_checkout` and
224
+ `billing.create_subscription_checkout` always send an `Idempotency-Key`: a new UUID per call,
225
+ reused across that call's retries, so a retry never creates or charges twice. To make re-running a
226
+ script safe too, pass your own stable key:
227
+
228
+ ```python
229
+ storeshot.screenshot_sets.create(body, idempotency_key="trailmate-release-2.4")
230
+ ```
231
+
232
+ The same key with the same body returns the original resource; with a different body it is
233
+ `409 IDEMPOTENCY_CONFLICT`.
234
+
235
+ ## Webhooks
236
+
237
+ Register an `https://` endpoint with `storeshot.webhooks.create({"url": url, "events": [...]})`
238
+ (the `whsec_…` secret is returned once), then verify every delivery against the **raw** body:
239
+
240
+ ```python
241
+ from flask import Flask, request
242
+ from storeshot import StoreshotWebhookError, construct_webhook_event
243
+
244
+ app = Flask(__name__)
245
+ SECRET = "…" # your whsec_ secret, from your secret store
246
+
247
+
248
+ @app.post("/hooks/storeshot")
249
+ def storeshot_hook() -> tuple[str, int]:
250
+ try:
251
+ event = construct_webhook_event(
252
+ request.get_data(), # the raw bytes, not parsed JSON
253
+ request.headers.get("Storeshot-Signature"),
254
+ SECRET,
255
+ )
256
+ except StoreshotWebhookError:
257
+ return "", 400
258
+ # Deduplicate on event["id"], then handle event["type"] / event["data"].
259
+ return "", 204
260
+ ```
261
+
262
+ With FastAPI or Starlette, pass `await request.body()`. `verify_webhook_signature(...)` returns a
263
+ `bool` instead of raising. The rules ([webhooks guide](https://storeshot.art/docs/webhooks/)):
264
+ `v1` is HMAC-SHA256 over `<t>.<raw body>`, any `v1` may match (two during a secret rotation),
265
+ compared in constant time; `t` must be within 5 minutes (`tolerance_seconds`). Pass
266
+ `[new_secret, old_secret]` while you roll your own configuration.
267
+
268
+ ## Test mode
269
+
270
+ With an `ss_test_…` key everything works like live (validation, uploads, queue, statuses,
271
+ idempotency, archives, webhooks to test webhooks) but no credits are used and no image model runs:
272
+ each result is a sample image of exactly the requested size. Test results are deleted after 30
273
+ days. Test and live are separate: a test key only sees test uploads, generations and webhooks.
274
+ `storeshot.me()["credential"]["environment"]` reports it.
275
+
276
+ ## Async frameworks
277
+
278
+ The client is synchronous (an async client is planned). In asyncio code, run calls in a thread:
279
+
280
+ ```python
281
+ import asyncio
282
+
283
+ me = await asyncio.to_thread(storeshot.me)
284
+ ```
285
+
286
+ ## Options
287
+
288
+ - `base_url` (or `STORESHOT_BASE_URL`): `https:`, or `http:` on localhost only. The key is sent
289
+ wherever it points.
290
+ - `http_client`: your own `httpx.Client` (proxies, TLS settings). The SDK never closes it; it must
291
+ not carry its own auth. Redirects are never followed.
292
+ - `default_headers`, `on_response`, and `client_identity={"surface": "cli", "name": "my-tool@1.0"}`
293
+ to label your tool's usage (`X-Storeshot-Client`, default `api/storeshot-sdk-python@<version>`).
294
+ - `with Storeshot() as storeshot:` (or `storeshot.close()`) closes the connection pool.
295
+ - Pyodide/browser pages are refused (`BROWSER_NOT_ALLOWED`): every visitor could read the key.
296
+ `dangerously_allow_browser=True` exists for internal tools only.
297
+
298
+ ## Method reference
299
+
300
+ Every API operation has exactly one method; every method takes keyword options last.
301
+
302
+ | Method | Endpoint | Notes |
303
+ | -------------------------------------- | ------------------------------------- | ------------------------ |
304
+ | `activity.list` | `GET /activity` | |
305
+ | `activity.list_all` | `GET /activity` | every page (Python only) |
306
+ | `api_keys.create` | `POST /api-keys` | |
307
+ | `api_keys.list` | `GET /api-keys` | |
308
+ | `api_keys.revoke` | `DELETE /api-keys/{id}` | |
309
+ | `auth.link_account` | `POST /auth/device/token` | whole device flow |
310
+ | `auth.poll_device_token` | `POST /auth/device/token` | no API key needed |
311
+ | `auth.start_device_authorization` | `POST /auth/device` | no API key needed |
312
+ | `billing.cancel_subscription` | `POST /billing/subscription/cancel` | |
313
+ | `billing.change_subscription` | `POST /billing/subscription/change` | |
314
+ | `billing.create_checkout` | `POST /billing/checkout` | auto Idempotency-Key |
315
+ | `billing.create_portal_session` | `POST /billing/portal` | |
316
+ | `billing.create_subscription_checkout` | `POST /billing/subscription/checkout` | auto Idempotency-Key |
317
+ | `billing.get_checkout` | `GET /billing/checkouts/{id}` | |
318
+ | `billing.get_subscription` | `GET /billing/subscription` | |
319
+ | `billing.list_packs` | `GET /billing/packs` | |
320
+ | `billing.list_plans` | `GET /billing/plans` | |
321
+ | `billing.uncancel_subscription` | `POST /billing/subscription/uncancel` | |
322
+ | `credits.get` | `GET /credits` | |
323
+ | `devices.list` | `GET /devices` | |
324
+ | `generations.cancel` | `POST /generations/{id}/cancel` | |
325
+ | `generations.create` | `POST /generations` | auto Idempotency-Key |
326
+ | `generations.delete` | `DELETE /generations/{id}` | |
327
+ | `generations.get` | `GET /generations/{id}` | |
328
+ | `generations.list` | `GET /generations` | |
329
+ | `generations.list_all` | `GET /generations` | every page (Python only) |
330
+ | `health` | `GET /health` | |
331
+ | `me` | `GET /me` | |
332
+ | `palettes.create` | `POST /palettes` | |
333
+ | `projects.add_items` | `POST /projects/{id}/items` | |
334
+ | `projects.create` | `POST /projects` | |
335
+ | `projects.delete` | `DELETE /projects/{id}` | |
336
+ | `projects.get` | `GET /projects/{id}` | |
337
+ | `projects.list` | `GET /projects` | |
338
+ | `projects.list_all` | `GET /projects` | every page (Python only) |
339
+ | `projects.remove_items` | `POST /projects/{id}/items/remove` | |
340
+ | `projects.update` | `PATCH /projects/{id}` | |
341
+ | `screenshot_sets.archive` | `GET /screenshot-sets/{id}/archive` | |
342
+ | `screenshot_sets.archive_stream` | `GET /screenshot-sets/{id}/archive` | |
343
+ | `screenshot_sets.cancel` | `POST /screenshot-sets/{id}/cancel` | |
344
+ | `screenshot_sets.create` | `POST /screenshot-sets` | auto Idempotency-Key |
345
+ | `screenshot_sets.delete` | `DELETE /screenshot-sets/{id}` | |
346
+ | `screenshot_sets.download_archive` | `GET /screenshot-sets/{id}/archive` | |
347
+ | `screenshot_sets.get` | `GET /screenshot-sets/{id}` | |
348
+ | `screenshot_sets.list` | `GET /screenshot-sets` | |
349
+ | `screenshot_sets.list_all` | `GET /screenshot-sets` | every page (Python only) |
350
+ | `screenshot_sets.quote` | `POST /screenshot-sets?dryRun=true` | free, creates nothing |
351
+ | `screenshot_sets.retry` | `POST /screenshot-sets/{id}/retry` | |
352
+ | `status` | `GET /status` | |
353
+ | `stores.list` | `GET /stores` | |
354
+ | `stores.requirements` | `GET /stores/{store}/requirements` | |
355
+ | `uploads.complete` | `POST /uploads/{id}/complete` | |
356
+ | `uploads.create` | `POST /uploads` | |
357
+ | `uploads.delete` | `DELETE /uploads/{id}` | |
358
+ | `uploads.get` | `GET /uploads/{id}` | |
359
+ | `uploads.list` | `GET /uploads` | |
360
+ | `uploads.list_all` | `GET /uploads` | every page (Python only) |
361
+ | `uploads.presign` | `POST /uploads/presign` | |
362
+ | `usage.get` | `GET /usage` | |
363
+ | `wait_for_generation` | `GET /generations/{id}` | |
364
+ | `wait_for_screenshot_set` | `GET /screenshot-sets/{id}` | |
365
+ | `webhooks.create` | `POST /webhooks` | |
366
+ | `webhooks.delete` | `DELETE /webhooks/{id}` | |
367
+ | `webhooks.get` | `GET /webhooks/{id}` | |
368
+ | `webhooks.list` | `GET /webhooks` | |
369
+ | `webhooks.list_all_deliveries` | `GET /webhooks/{id}/deliveries` | every page (Python only) |
370
+ | `webhooks.list_deliveries` | `GET /webhooks/{id}/deliveries` | |
371
+ | `webhooks.rotate_secret` | `POST /webhooks/{id}/rotate-secret` | |
372
+ | `webhooks.test` | `POST /webhooks/{id}/test` | |
373
+ | `webhooks.update` | `PATCH /webhooks/{id}` | |
374
+
375
+ ## Versioning
376
+
377
+ `storeshot.__version__` is the package version (0.x until 1.0.0; a 0.x minor may change the
378
+ surface). `storeshot.CONTRACT_VERSION` is the API contract the types were generated from (today
379
+ 1.17.0); the API only changes additively within a major version, so a newer API keeps working with
380
+ this SDK. See `CHANGELOG.md`.
381
+
382
+ Licensed under the Apache License 2.0 (`LICENSE`, `NOTICE`).