webclaw 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.
- webclaw-0.1.0/.github/banner.png +0 -0
- webclaw-0.1.0/.gitignore +14 -0
- webclaw-0.1.0/LICENSE +21 -0
- webclaw-0.1.0/PKG-INFO +435 -0
- webclaw-0.1.0/README.md +406 -0
- webclaw-0.1.0/pyproject.toml +37 -0
- webclaw-0.1.0/tests/__init__.py +0 -0
- webclaw-0.1.0/tests/test_async_client.py +268 -0
- webclaw-0.1.0/tests/test_client.py +395 -0
- webclaw-0.1.0/webclaw/__init__.py +72 -0
- webclaw-0.1.0/webclaw/_endpoints.py +264 -0
- webclaw-0.1.0/webclaw/async_client.py +210 -0
- webclaw-0.1.0/webclaw/client.py +229 -0
- webclaw-0.1.0/webclaw/errors.py +39 -0
- webclaw-0.1.0/webclaw/py.typed +0 -0
- webclaw-0.1.0/webclaw/types.py +210 -0
|
Binary file
|
webclaw-0.1.0/.gitignore
ADDED
webclaw-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 webclaw
|
|
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.
|
webclaw-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webclaw
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Webclaw web extraction API
|
|
5
|
+
Project-URL: Homepage, https://webclaw.io
|
|
6
|
+
Project-URL: Documentation, https://webclaw.io/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/0xMassi/webclaw-python
|
|
8
|
+
Project-URL: Issues, https://github.com/0xMassi/webclaw-python/issues
|
|
9
|
+
Author: webclaw
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: httpx>=0.24.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<a href="https://webclaw.io">
|
|
32
|
+
<img src=".github/banner.png" alt="webclaw" width="600" />
|
|
33
|
+
</a>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<strong>Python SDK for the Webclaw web extraction API</strong>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<a href="https://pypi.org/project/webclaw"><img src="https://img.shields.io/pypi/v/webclaw?style=flat-square&color=212529" alt="PyPI" /></a>
|
|
42
|
+
<a href="https://pypi.org/project/webclaw"><img src="https://img.shields.io/pypi/pyversions/webclaw?style=flat-square&color=212529" alt="Python" /></a>
|
|
43
|
+
<a href="https://github.com/0xMassi/webclaw-python/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-212529?style=flat-square" alt="License" /></a>
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
> **Note**: The webclaw Cloud API is currently in closed beta. [Request early access](https://webclaw.io) or use the [open-source CLI/MCP](https://github.com/0xMassi/webclaw) for local extraction.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install webclaw
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Requires Python 3.9+. The only dependency is [httpx](https://www.python-httpx.org/).
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
### Sync
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from webclaw import Webclaw
|
|
66
|
+
|
|
67
|
+
client = Webclaw("wc-YOUR_API_KEY")
|
|
68
|
+
|
|
69
|
+
result = client.scrape("https://example.com", formats=["markdown"])
|
|
70
|
+
print(result.markdown)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Async
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from webclaw import AsyncWebclaw
|
|
77
|
+
|
|
78
|
+
async with AsyncWebclaw("wc-YOUR_API_KEY") as client:
|
|
79
|
+
result = await client.scrape("https://example.com", formats=["markdown"])
|
|
80
|
+
print(result.markdown)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Both clients support identical method signatures. Every sync method has an async equivalent. The examples below use the sync client for brevity.
|
|
84
|
+
|
|
85
|
+
## Endpoints
|
|
86
|
+
|
|
87
|
+
### Scrape
|
|
88
|
+
|
|
89
|
+
Extract content from a single URL. Supports multiple output formats: `"markdown"`, `"text"`, `"llm"`, `"json"`.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
result = client.scrape(
|
|
93
|
+
"https://example.com",
|
|
94
|
+
formats=["markdown", "text", "llm"],
|
|
95
|
+
include_selectors=["article", ".content"],
|
|
96
|
+
exclude_selectors=["nav", "footer"],
|
|
97
|
+
only_main_content=True,
|
|
98
|
+
no_cache=True,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
result.url # str
|
|
102
|
+
result.markdown # str | None
|
|
103
|
+
result.text # str | None
|
|
104
|
+
result.llm # str | None
|
|
105
|
+
result.json_data # Any | None
|
|
106
|
+
result.metadata # dict
|
|
107
|
+
result.cache # CacheInfo | None (.status: "hit" | "miss" | "bypass")
|
|
108
|
+
result.warning # str | None
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Search
|
|
112
|
+
|
|
113
|
+
Web search with optional topic filtering.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
results = client.search("web scraping tools 2026", num_results=10, topic="tech")
|
|
117
|
+
|
|
118
|
+
for r in results["results"]:
|
|
119
|
+
print(r["title"], r["url"])
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Parameters:** `query` (str), `num_results` (int, optional), `topic` (str, optional).
|
|
123
|
+
|
|
124
|
+
### Map
|
|
125
|
+
|
|
126
|
+
Discover URLs via sitemap.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
result = client.map("https://example.com")
|
|
130
|
+
|
|
131
|
+
print(result.count)
|
|
132
|
+
for url in result.urls:
|
|
133
|
+
print(url)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Batch
|
|
137
|
+
|
|
138
|
+
Scrape multiple URLs in parallel.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
result = client.batch(
|
|
142
|
+
["https://a.com", "https://b.com", "https://c.com"],
|
|
143
|
+
formats=["markdown"],
|
|
144
|
+
concurrency=5,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
for item in result.results:
|
|
148
|
+
print(item.url, item.markdown, item.error or "ok")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Parameters:** `urls` (list[str]), `formats` (optional), `concurrency` (int, default 5).
|
|
152
|
+
|
|
153
|
+
### Extract
|
|
154
|
+
|
|
155
|
+
LLM-powered structured data extraction. Use either a JSON schema or a natural language prompt.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# Schema-based extraction
|
|
159
|
+
result = client.extract(
|
|
160
|
+
"https://example.com/pricing",
|
|
161
|
+
schema={
|
|
162
|
+
"type": "object",
|
|
163
|
+
"properties": {
|
|
164
|
+
"plans": {
|
|
165
|
+
"type": "array",
|
|
166
|
+
"items": {
|
|
167
|
+
"type": "object",
|
|
168
|
+
"properties": {
|
|
169
|
+
"name": {"type": "string"},
|
|
170
|
+
"price": {"type": "string"},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
print(result.data) # dict matching your schema
|
|
178
|
+
|
|
179
|
+
# Prompt-based extraction
|
|
180
|
+
result = client.extract(
|
|
181
|
+
"https://example.com/pricing",
|
|
182
|
+
prompt="Extract all pricing tiers with names and monthly prices",
|
|
183
|
+
)
|
|
184
|
+
print(result.data)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Summarize
|
|
188
|
+
|
|
189
|
+
Summarize page content with an optional sentence limit.
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
result = client.summarize("https://example.com", max_sentences=3)
|
|
193
|
+
print(result.summary)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Diff
|
|
197
|
+
|
|
198
|
+
Detect content changes at a URL since the last check.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
result = client.diff("https://example.com/status")
|
|
202
|
+
|
|
203
|
+
print(result["has_changed"]) # bool
|
|
204
|
+
print(result["diff"]) # str, unified diff of changes
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Brand
|
|
208
|
+
|
|
209
|
+
Extract brand identity (colors, fonts, logos) from a URL.
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
result = client.brand("https://example.com")
|
|
213
|
+
print(result.data) # dict with brand identity fields
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Agent Scrape
|
|
217
|
+
|
|
218
|
+
AI-guided scraping that navigates a page to achieve a specified goal.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
result = client.agent_scrape(
|
|
222
|
+
"https://example.com/dashboard",
|
|
223
|
+
goal="Find the monthly active users count",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
print(result["result"])
|
|
227
|
+
print(result["steps"])
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Parameters:** `url` (str), `goal` (str), plus optional keyword arguments forwarded to the API.
|
|
231
|
+
|
|
232
|
+
### Research
|
|
233
|
+
|
|
234
|
+
Deep research that searches, reads, and synthesizes information from multiple sources. This is an async job: the SDK starts it and polls until completion.
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
# Blocks until research completes (up to 600s, or 1200s with deep=True)
|
|
238
|
+
result = client.research(
|
|
239
|
+
"How do modern web crawlers handle JavaScript rendering?",
|
|
240
|
+
max_sources=15,
|
|
241
|
+
deep=True,
|
|
242
|
+
topic="tech",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
print(result.report)
|
|
246
|
+
print(result.iterations)
|
|
247
|
+
print(result.elapsed_ms)
|
|
248
|
+
|
|
249
|
+
for source in result.sources:
|
|
250
|
+
print(source["url"], source["title"])
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
To check status without blocking:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
status = client.get_research_status("job-id-here")
|
|
257
|
+
print(status.status) # "running" | "completed" | "failed"
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Parameters:** `query` (str), `deep` (bool, default False), `max_sources` (int, optional), `max_iterations` (int, optional), `topic` (str, optional).
|
|
261
|
+
|
|
262
|
+
### Crawl
|
|
263
|
+
|
|
264
|
+
Start an async crawl that follows links from a seed URL.
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
job = client.crawl(
|
|
268
|
+
"https://example.com",
|
|
269
|
+
max_depth=3,
|
|
270
|
+
max_pages=100,
|
|
271
|
+
use_sitemap=True,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Poll until complete (default timeout 300s)
|
|
275
|
+
status = job.wait(interval=2.0, timeout=300.0)
|
|
276
|
+
|
|
277
|
+
print(status.total, status.completed, status.errors)
|
|
278
|
+
for page in status.pages:
|
|
279
|
+
print(page.url, len(page.markdown or ""))
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Check status without waiting:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
status = job.get_status()
|
|
286
|
+
print(status.status) # "running" | "completed" | "failed"
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Async variant:
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
job = await client.crawl("https://example.com", max_depth=2)
|
|
293
|
+
status = await job.wait()
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Watch
|
|
297
|
+
|
|
298
|
+
Monitor URLs for content changes with automatic periodic checks.
|
|
299
|
+
|
|
300
|
+
**Create a watch:**
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
watch = client.watch_create(
|
|
304
|
+
"https://example.com/pricing",
|
|
305
|
+
name="Pricing page monitor",
|
|
306
|
+
interval_minutes=60,
|
|
307
|
+
webhook_url="https://hooks.example.com/webclaw",
|
|
308
|
+
)
|
|
309
|
+
print(watch.id, watch.status)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**List all watches:**
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
result = client.watch_list(limit=50, offset=0)
|
|
316
|
+
for w in result.watches:
|
|
317
|
+
print(w.id, w.url, w.name, w.last_checked)
|
|
318
|
+
print(result.total)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Get a single watch:**
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
watch = client.watch_get("watch-id-here")
|
|
325
|
+
print(watch.url, watch.interval_minutes)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Delete a watch:**
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
client.watch_delete("watch-id-here")
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Trigger an immediate check:**
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
check = client.watch_check("watch-id-here")
|
|
338
|
+
print(check.has_changed) # bool
|
|
339
|
+
print(check.diff) # str | None
|
|
340
|
+
print(check.checked_at) # ISO timestamp
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Error Handling
|
|
344
|
+
|
|
345
|
+
All errors inherit from `WebclawError`, which carries the HTTP status code when available.
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
from webclaw import (
|
|
349
|
+
WebclawError,
|
|
350
|
+
AuthenticationError,
|
|
351
|
+
NotFoundError,
|
|
352
|
+
RateLimitError,
|
|
353
|
+
TimeoutError,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
result = client.scrape("https://example.com")
|
|
358
|
+
except AuthenticationError:
|
|
359
|
+
print("Invalid or missing API key")
|
|
360
|
+
except RateLimitError:
|
|
361
|
+
print("Too many requests, slow down")
|
|
362
|
+
except NotFoundError:
|
|
363
|
+
print("Resource not found")
|
|
364
|
+
except TimeoutError as e:
|
|
365
|
+
print(f"Operation timed out: {e}")
|
|
366
|
+
except WebclawError as e:
|
|
367
|
+
print(f"API error (status {e.status_code}): {e}")
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
| Exception | HTTP Status | When |
|
|
371
|
+
|-----------|-------------|------|
|
|
372
|
+
| `AuthenticationError` | 401 / 403 | Invalid or missing API key |
|
|
373
|
+
| `NotFoundError` | 404 | Resource does not exist |
|
|
374
|
+
| `RateLimitError` | 429 | Too many requests |
|
|
375
|
+
| `TimeoutError` | -- | Crawl/research polling exceeded timeout |
|
|
376
|
+
| `WebclawError` | Any | Base class for all other API errors |
|
|
377
|
+
|
|
378
|
+
## Configuration
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
import os
|
|
382
|
+
from webclaw import Webclaw
|
|
383
|
+
|
|
384
|
+
client = Webclaw(
|
|
385
|
+
os.environ["WEBCLAW_API_KEY"],
|
|
386
|
+
base_url="https://api.webclaw.io", # default
|
|
387
|
+
timeout=60.0, # seconds, default 30
|
|
388
|
+
)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Both `Webclaw` and `AsyncWebclaw` support context managers for automatic cleanup:
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
# Sync
|
|
395
|
+
with Webclaw("wc-YOUR_API_KEY") as client:
|
|
396
|
+
result = client.scrape("https://example.com")
|
|
397
|
+
|
|
398
|
+
# Async
|
|
399
|
+
async with AsyncWebclaw("wc-YOUR_API_KEY") as client:
|
|
400
|
+
result = await client.scrape("https://example.com")
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Async Usage
|
|
404
|
+
|
|
405
|
+
Every endpoint is available on `AsyncWebclaw` with identical parameters. Use `await` on all method calls and `async with` for the context manager.
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
import asyncio
|
|
409
|
+
from webclaw import AsyncWebclaw
|
|
410
|
+
|
|
411
|
+
async def main():
|
|
412
|
+
async with AsyncWebclaw("wc-YOUR_API_KEY") as client:
|
|
413
|
+
# Run multiple scrapes concurrently
|
|
414
|
+
results = await asyncio.gather(
|
|
415
|
+
client.scrape("https://a.com", formats=["markdown"]),
|
|
416
|
+
client.scrape("https://b.com", formats=["markdown"]),
|
|
417
|
+
client.scrape("https://c.com", formats=["markdown"]),
|
|
418
|
+
)
|
|
419
|
+
for r in results:
|
|
420
|
+
print(r.url, len(r.markdown or ""))
|
|
421
|
+
|
|
422
|
+
asyncio.run(main())
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Type Support
|
|
426
|
+
|
|
427
|
+
This package ships with a `py.typed` marker (PEP 561). Type checkers like mypy and pyright will pick up all type annotations automatically. All response types are dataclasses importable from the top-level package:
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
from webclaw import ScrapeResponse, CrawlStatus, MapResponse, ExtractResponse
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## License
|
|
434
|
+
|
|
435
|
+
MIT
|