screenshotdude 1.0.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,17 @@
1
+ node_modules
2
+ .next
3
+ dist
4
+ .turbo
5
+ .env
6
+ .env.local
7
+ *.log
8
+ coverage
9
+ storage-data
10
+ __pycache__
11
+ *.egg-info
12
+ .venv
13
+ *.tsbuildinfo
14
+ .vercel
15
+
16
+ # npm pack output
17
+ *.tgz
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ScreenshotDude
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,229 @@
1
+ Metadata-Version: 2.5
2
+ Name: screenshotdude
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for the ScreenshotDude screenshot, content and AI-analysis API.
5
+ Project-URL: Homepage, https://screenshotdude.com
6
+ Project-URL: Documentation, https://screenshotdude.com/docs
7
+ Author: ScreenshotDude
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: pdf,screenshot,screenshot-api,screenshotdude,sdk,web-scraping
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.9
18
+ Requires-Dist: httpx<1,>=0.24
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
21
+ Requires-Dist: pytest>=7; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # screenshotdude
25
+
26
+ Official Python SDK for the [ScreenshotDude](https://screenshotdude.com) API — screenshots & PDFs, content extraction,
27
+ AI analysis, raw-HTML crawling and async jobs. Sync and asyncio clients, fully typed, built on `httpx`.
28
+
29
+ ```bash
30
+ pip install screenshotdude
31
+ ```
32
+
33
+ Requires Python 3.9+.
34
+
35
+ ## Quick start
36
+
37
+ ```python
38
+ from screenshotdude import ScreenshotDude
39
+
40
+ client = ScreenshotDude(api_key="sl_live_...") # or set SCREENSHOTDUDE_API_KEY
41
+
42
+ # Screenshot straight to disk
43
+ client.capture_to_file("https://example.com", "example.png", full_page=True)
44
+
45
+ # Screenshot in memory
46
+ shot = client.capture("https://example.com", device="iphone_14_pro", format="webp")
47
+ print(shot.content_type, len(shot.content), shot.credits_charged, shot.request_id, shot.cached)
48
+ ```
49
+
50
+ ### Async
51
+
52
+ ```python
53
+ import asyncio
54
+ from screenshotdude import AsyncScreenshotDude
55
+
56
+ async def main():
57
+ async with AsyncScreenshotDude() as client:
58
+ page = await client.content("https://example.com", include_markdown=True)
59
+ print(page["data"]["markdown"])
60
+
61
+ asyncio.run(main())
62
+ ```
63
+
64
+ `AsyncScreenshotDude` has the same methods as `ScreenshotDude`, all awaitable.
65
+
66
+ ## Configuration
67
+
68
+ ```python
69
+ ScreenshotDude(
70
+ api_key=None, # default: $SCREENSHOTDUDE_API_KEY (sent as the x-api-key header)
71
+ base_url=None, # default: $SCREENSHOTDUDE_BASE_URL or https://api.screenshotdude.com
72
+ timeout=65, # seconds, per attempt
73
+ max_retries=3,
74
+ retry_delay=1.0, # seconds; delay = retry_delay * 1.5 ** attempt
75
+ transport=None, # custom httpx transport (e.g. httpx.MockTransport for tests)
76
+ http_client=None, # or bring your own httpx.Client / httpx.AsyncClient
77
+ )
78
+ ```
79
+
80
+ Every endpoint method accepts `request_options={"max_retries": 0, "timeout": 30}` to override per request.
81
+ Use the client as a context manager (or call `close()` / `await aclose()`) to release connections.
82
+
83
+ ## Endpoints
84
+
85
+ Parameters are passed as keyword arguments with the API's names (see `screenshotdude.types` for typed,
86
+ documented `TypedDict`s). Booleans are sent as `"true"`/`"false"`, lists comma-joined
87
+ (`block_resources=["image", "font"]`), and dicts JSON-encoded (`headers`, `cookies`, `custom_response`).
88
+
89
+ ### Capture — `GET /v1/capture`
90
+
91
+ ```python
92
+ res = client.capture(
93
+ "https://example.com",
94
+ format="jpeg",
95
+ image_quality=80,
96
+ viewport_width=1440,
97
+ block_resources=["font", "media"],
98
+ wait_until=["networkidle0"],
99
+ headers={"Accept-Language": "fr-FR"},
100
+ cookies=[{"name": "session", "value": "abc", "domain": "example.com"}],
101
+ remove_cookie_banners=True,
102
+ dark_mode=True,
103
+ )
104
+ res.save("example.jpg")
105
+ ```
106
+
107
+ Returns a `CaptureResult(content: bytes, content_type, request_id, credits_charged, cached)`, or a `dict`
108
+ when the API answers with JSON: `s3_url=True` → `{"success": True, "data": {"url", "key", "bucket"}}`,
109
+ `is_async=True` → `{"job_id": ...}`.
110
+
111
+ ### Content — `GET /v1/content`
112
+
113
+ ```python
114
+ page = client.content("https://example.com", include_markdown=True, use_defuddle=True)
115
+ print(page["data"]["metadata"]["title"], page["data"]["links"]["external"])
116
+ ```
117
+
118
+ ### Analyze — `GET /v1/analyze`
119
+
120
+ ```python
121
+ insights = client.analyze(
122
+ "https://example.com",
123
+ custom_prompt="Focus on pricing",
124
+ custom_response={"sentiment": "overall sentiment of the page"},
125
+ )
126
+ print(insights["data"]["summary"], insights["data"]["keyFeatures"], insights["data"]["sentiment"])
127
+ ```
128
+
129
+ ### Crawler — `GET /v1/website/crawler`
130
+
131
+ ```python
132
+ info = client.crawler("https://example.com", allow_redirects=False) # dict
133
+ html = client.crawler("https://example.com", return_page_source=True) # str
134
+ ```
135
+
136
+ ### Async jobs
137
+
138
+ ```python
139
+ accepted = client.capture("https://example.com", full_page=True, is_async=True)
140
+ job = client.wait_for_job(
141
+ accepted["job_id"],
142
+ interval=2, # first poll delay in seconds; grows x1.5 up to 60s
143
+ timeout=300,
144
+ on_progress=lambda j: print(j["status"], j["progress"]),
145
+ )
146
+ print(job["result"]) # capture jobs: {"url", "content_type", "size"} — a download URL
147
+ ```
148
+
149
+ `wait_for_job` raises `ScreenshotDudeJobError` (with `.job`) when the job ends in `Error`, `NotFound` or
150
+ `BadRequest`, or on timeout. `client.job(job_id)` polls once (free).
151
+
152
+ ### Account
153
+
154
+ ```python
155
+ client.usage() # GET /v1/usage
156
+ client.stats(engine="capture") # GET /v1/stats?engine=capture
157
+ ```
158
+
159
+ ### Anything else
160
+
161
+ ```python
162
+ res = client.request("GET", "/v1/usage", {"some": "param"})
163
+ print(res.data, res.status, res.request_id, res.credits_charged, res.cached)
164
+ ```
165
+
166
+ ### Batching
167
+
168
+ ```python
169
+ urls = ["https://a.com", "https://b.com"]
170
+ for r in client.batch(urls, lambda u: client.content(u), concurrency=5):
171
+ print(urls[r.index], r.status, r.value if r.status == "fulfilled" else r.reason)
172
+
173
+ # async: results = await aclient.batch(urls, lambda u: aclient.content(u), concurrency=5)
174
+ ```
175
+
176
+ Failures don't stop the other items; results come back in input order.
177
+
178
+ ## Errors & retries
179
+
180
+ ```python
181
+ from screenshotdude import ScreenshotDudeApiError, ScreenshotDudeConnectionError
182
+
183
+ try:
184
+ client.capture("https://example.com")
185
+ except ScreenshotDudeApiError as e:
186
+ print(e.status, e.message, e.request_id, e.response_body)
187
+ if e.status == 402:
188
+ print("Out of credits")
189
+ elif e.is_quota_exceeded:
190
+ print("API key quota reached")
191
+ except ScreenshotDudeConnectionError as e:
192
+ print("Network problem:", e)
193
+ ```
194
+
195
+ Retried automatically (up to `max_retries`, delay `retry_delay * 1.5 ** attempt`; a `Retry-After` header wins):
196
+ network errors and timeouts, `408`, `500`, `502`, `503`, `504`, and `401` plan rate limits
197
+ (`Rate limit exceeded: ...`). Never retried: `402`, per-key quotas (`... quota ...`) and other `4xx`.
198
+
199
+ With `soft_fail=True` the API always answers HTTP 200 and failures come back as `{"error": ...}`.
200
+
201
+ ## Webhooks
202
+
203
+ With `is_async=True` and `webhook_url=...`, ScreenshotDude POSTs the finished job to your URL, signed with
204
+ `t=<unix>,v1=<hex hmac-sha256(secret, "<t>.<body>")>`. Verify against the **raw** request body:
205
+
206
+ ```python
207
+ from screenshotdude import verify_webhook
208
+
209
+ @app.post("/screenshotdude-webhook")
210
+ async def hook(request: Request):
211
+ body = await request.body()
212
+ if not verify_webhook(WEBHOOK_SECRET, body, request.headers.get("x-snaplens-signature")):
213
+ raise HTTPException(400)
214
+ job = json.loads(body)
215
+ ```
216
+
217
+ `verify_webhook(secret, body, header, tolerance=300)` uses a constant-time comparison and rejects
218
+ signatures older than `tolerance` seconds.
219
+
220
+ ## Development
221
+
222
+ ```bash
223
+ python -m venv .venv && .venv/bin/pip install -e ".[dev]"
224
+ .venv/bin/pytest
225
+ ```
226
+
227
+ ## License
228
+
229
+ MIT
@@ -0,0 +1,206 @@
1
+ # screenshotdude
2
+
3
+ Official Python SDK for the [ScreenshotDude](https://screenshotdude.com) API — screenshots & PDFs, content extraction,
4
+ AI analysis, raw-HTML crawling and async jobs. Sync and asyncio clients, fully typed, built on `httpx`.
5
+
6
+ ```bash
7
+ pip install screenshotdude
8
+ ```
9
+
10
+ Requires Python 3.9+.
11
+
12
+ ## Quick start
13
+
14
+ ```python
15
+ from screenshotdude import ScreenshotDude
16
+
17
+ client = ScreenshotDude(api_key="sl_live_...") # or set SCREENSHOTDUDE_API_KEY
18
+
19
+ # Screenshot straight to disk
20
+ client.capture_to_file("https://example.com", "example.png", full_page=True)
21
+
22
+ # Screenshot in memory
23
+ shot = client.capture("https://example.com", device="iphone_14_pro", format="webp")
24
+ print(shot.content_type, len(shot.content), shot.credits_charged, shot.request_id, shot.cached)
25
+ ```
26
+
27
+ ### Async
28
+
29
+ ```python
30
+ import asyncio
31
+ from screenshotdude import AsyncScreenshotDude
32
+
33
+ async def main():
34
+ async with AsyncScreenshotDude() as client:
35
+ page = await client.content("https://example.com", include_markdown=True)
36
+ print(page["data"]["markdown"])
37
+
38
+ asyncio.run(main())
39
+ ```
40
+
41
+ `AsyncScreenshotDude` has the same methods as `ScreenshotDude`, all awaitable.
42
+
43
+ ## Configuration
44
+
45
+ ```python
46
+ ScreenshotDude(
47
+ api_key=None, # default: $SCREENSHOTDUDE_API_KEY (sent as the x-api-key header)
48
+ base_url=None, # default: $SCREENSHOTDUDE_BASE_URL or https://api.screenshotdude.com
49
+ timeout=65, # seconds, per attempt
50
+ max_retries=3,
51
+ retry_delay=1.0, # seconds; delay = retry_delay * 1.5 ** attempt
52
+ transport=None, # custom httpx transport (e.g. httpx.MockTransport for tests)
53
+ http_client=None, # or bring your own httpx.Client / httpx.AsyncClient
54
+ )
55
+ ```
56
+
57
+ Every endpoint method accepts `request_options={"max_retries": 0, "timeout": 30}` to override per request.
58
+ Use the client as a context manager (or call `close()` / `await aclose()`) to release connections.
59
+
60
+ ## Endpoints
61
+
62
+ Parameters are passed as keyword arguments with the API's names (see `screenshotdude.types` for typed,
63
+ documented `TypedDict`s). Booleans are sent as `"true"`/`"false"`, lists comma-joined
64
+ (`block_resources=["image", "font"]`), and dicts JSON-encoded (`headers`, `cookies`, `custom_response`).
65
+
66
+ ### Capture — `GET /v1/capture`
67
+
68
+ ```python
69
+ res = client.capture(
70
+ "https://example.com",
71
+ format="jpeg",
72
+ image_quality=80,
73
+ viewport_width=1440,
74
+ block_resources=["font", "media"],
75
+ wait_until=["networkidle0"],
76
+ headers={"Accept-Language": "fr-FR"},
77
+ cookies=[{"name": "session", "value": "abc", "domain": "example.com"}],
78
+ remove_cookie_banners=True,
79
+ dark_mode=True,
80
+ )
81
+ res.save("example.jpg")
82
+ ```
83
+
84
+ Returns a `CaptureResult(content: bytes, content_type, request_id, credits_charged, cached)`, or a `dict`
85
+ when the API answers with JSON: `s3_url=True` → `{"success": True, "data": {"url", "key", "bucket"}}`,
86
+ `is_async=True` → `{"job_id": ...}`.
87
+
88
+ ### Content — `GET /v1/content`
89
+
90
+ ```python
91
+ page = client.content("https://example.com", include_markdown=True, use_defuddle=True)
92
+ print(page["data"]["metadata"]["title"], page["data"]["links"]["external"])
93
+ ```
94
+
95
+ ### Analyze — `GET /v1/analyze`
96
+
97
+ ```python
98
+ insights = client.analyze(
99
+ "https://example.com",
100
+ custom_prompt="Focus on pricing",
101
+ custom_response={"sentiment": "overall sentiment of the page"},
102
+ )
103
+ print(insights["data"]["summary"], insights["data"]["keyFeatures"], insights["data"]["sentiment"])
104
+ ```
105
+
106
+ ### Crawler — `GET /v1/website/crawler`
107
+
108
+ ```python
109
+ info = client.crawler("https://example.com", allow_redirects=False) # dict
110
+ html = client.crawler("https://example.com", return_page_source=True) # str
111
+ ```
112
+
113
+ ### Async jobs
114
+
115
+ ```python
116
+ accepted = client.capture("https://example.com", full_page=True, is_async=True)
117
+ job = client.wait_for_job(
118
+ accepted["job_id"],
119
+ interval=2, # first poll delay in seconds; grows x1.5 up to 60s
120
+ timeout=300,
121
+ on_progress=lambda j: print(j["status"], j["progress"]),
122
+ )
123
+ print(job["result"]) # capture jobs: {"url", "content_type", "size"} — a download URL
124
+ ```
125
+
126
+ `wait_for_job` raises `ScreenshotDudeJobError` (with `.job`) when the job ends in `Error`, `NotFound` or
127
+ `BadRequest`, or on timeout. `client.job(job_id)` polls once (free).
128
+
129
+ ### Account
130
+
131
+ ```python
132
+ client.usage() # GET /v1/usage
133
+ client.stats(engine="capture") # GET /v1/stats?engine=capture
134
+ ```
135
+
136
+ ### Anything else
137
+
138
+ ```python
139
+ res = client.request("GET", "/v1/usage", {"some": "param"})
140
+ print(res.data, res.status, res.request_id, res.credits_charged, res.cached)
141
+ ```
142
+
143
+ ### Batching
144
+
145
+ ```python
146
+ urls = ["https://a.com", "https://b.com"]
147
+ for r in client.batch(urls, lambda u: client.content(u), concurrency=5):
148
+ print(urls[r.index], r.status, r.value if r.status == "fulfilled" else r.reason)
149
+
150
+ # async: results = await aclient.batch(urls, lambda u: aclient.content(u), concurrency=5)
151
+ ```
152
+
153
+ Failures don't stop the other items; results come back in input order.
154
+
155
+ ## Errors & retries
156
+
157
+ ```python
158
+ from screenshotdude import ScreenshotDudeApiError, ScreenshotDudeConnectionError
159
+
160
+ try:
161
+ client.capture("https://example.com")
162
+ except ScreenshotDudeApiError as e:
163
+ print(e.status, e.message, e.request_id, e.response_body)
164
+ if e.status == 402:
165
+ print("Out of credits")
166
+ elif e.is_quota_exceeded:
167
+ print("API key quota reached")
168
+ except ScreenshotDudeConnectionError as e:
169
+ print("Network problem:", e)
170
+ ```
171
+
172
+ Retried automatically (up to `max_retries`, delay `retry_delay * 1.5 ** attempt`; a `Retry-After` header wins):
173
+ network errors and timeouts, `408`, `500`, `502`, `503`, `504`, and `401` plan rate limits
174
+ (`Rate limit exceeded: ...`). Never retried: `402`, per-key quotas (`... quota ...`) and other `4xx`.
175
+
176
+ With `soft_fail=True` the API always answers HTTP 200 and failures come back as `{"error": ...}`.
177
+
178
+ ## Webhooks
179
+
180
+ With `is_async=True` and `webhook_url=...`, ScreenshotDude POSTs the finished job to your URL, signed with
181
+ `t=<unix>,v1=<hex hmac-sha256(secret, "<t>.<body>")>`. Verify against the **raw** request body:
182
+
183
+ ```python
184
+ from screenshotdude import verify_webhook
185
+
186
+ @app.post("/screenshotdude-webhook")
187
+ async def hook(request: Request):
188
+ body = await request.body()
189
+ if not verify_webhook(WEBHOOK_SECRET, body, request.headers.get("x-snaplens-signature")):
190
+ raise HTTPException(400)
191
+ job = json.loads(body)
192
+ ```
193
+
194
+ `verify_webhook(secret, body, header, tolerance=300)` uses a constant-time comparison and rejects
195
+ signatures older than `tolerance` seconds.
196
+
197
+ ## Development
198
+
199
+ ```bash
200
+ python -m venv .venv && .venv/bin/pip install -e ".[dev]"
201
+ .venv/bin/pytest
202
+ ```
203
+
204
+ ## License
205
+
206
+ MIT
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "screenshotdude"
7
+ version = "1.0.0"
8
+ description = "Official Python SDK for the ScreenshotDude screenshot, content and AI-analysis API."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "ScreenshotDude" }]
13
+ keywords = ["screenshotdude", "screenshot", "screenshot-api", "pdf", "web-scraping", "sdk"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Typing :: Typed",
20
+ "Framework :: AsyncIO",
21
+ ]
22
+ dependencies = ["httpx>=0.24,<1"]
23
+
24
+ [project.optional-dependencies]
25
+ dev = ["pytest>=7", "pytest-asyncio>=0.21"]
26
+
27
+ [project.urls]
28
+ Homepage = "https://screenshotdude.com"
29
+ Documentation = "https://screenshotdude.com/docs"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["screenshotdude"]
33
+
34
+ [tool.hatch.build.targets.sdist]
35
+ include = ["screenshotdude", "tests", "README.md", "LICENSE", "pyproject.toml"]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
39
+ asyncio_mode = "auto"
40
+ asyncio_default_fixture_loop_scope = "function"
@@ -0,0 +1,55 @@
1
+ """Official Python SDK for the ScreenshotDude screenshot, content and AI-analysis API.
2
+
3
+ Example::
4
+
5
+ from screenshotdude import ScreenshotDude
6
+
7
+ with ScreenshotDude() as client: # reads SCREENSHOTDUDE_API_KEY
8
+ client.capture_to_file("https://example.com", "example.png", full_page=True)
9
+ """
10
+
11
+ from ._client import (
12
+ DEFAULT_BASE_URL,
13
+ AsyncScreenshotDude,
14
+ ScreenshotDude,
15
+ __version__,
16
+ build_query,
17
+ is_retryable,
18
+ parse_retry_after,
19
+ )
20
+ from ._errors import ScreenshotDudeApiError, ScreenshotDudeConnectionError, ScreenshotDudeError, ScreenshotDudeJobError
21
+ from ._webhook import compute_webhook_signature, verify_webhook
22
+ from .types import ApiResponse, BatchResult, CaptureResult
23
+
24
+ # Deprecated aliases for the pre-rebrand names. They will be removed in a future major version.
25
+ Snaplens = ScreenshotDude
26
+ AsyncSnaplens = AsyncScreenshotDude
27
+ SnaplensError = ScreenshotDudeError
28
+ SnaplensApiError = ScreenshotDudeApiError
29
+ SnaplensConnectionError = ScreenshotDudeConnectionError
30
+ SnaplensJobError = ScreenshotDudeJobError
31
+
32
+ __all__ = [
33
+ "ScreenshotDude",
34
+ "AsyncScreenshotDude",
35
+ "CaptureResult",
36
+ "ApiResponse",
37
+ "BatchResult",
38
+ "ScreenshotDudeError",
39
+ "ScreenshotDudeApiError",
40
+ "ScreenshotDudeConnectionError",
41
+ "ScreenshotDudeJobError",
42
+ "verify_webhook",
43
+ "compute_webhook_signature",
44
+ "build_query",
45
+ "is_retryable",
46
+ "parse_retry_after",
47
+ "DEFAULT_BASE_URL",
48
+ "__version__",
49
+ "Snaplens",
50
+ "AsyncSnaplens",
51
+ "SnaplensError",
52
+ "SnaplensApiError",
53
+ "SnaplensConnectionError",
54
+ "SnaplensJobError",
55
+ ]