xdive 1.5.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.
- xdive-1.5.0/.gitignore +76 -0
- xdive-1.5.0/CHANGELOG.md +75 -0
- xdive-1.5.0/LICENSE +21 -0
- xdive-1.5.0/PKG-INFO +185 -0
- xdive-1.5.0/README.md +157 -0
- xdive-1.5.0/pyproject.toml +38 -0
- xdive-1.5.0/tests/test_client.py +327 -0
- xdive-1.5.0/xdive/__init__.py +31 -0
- xdive-1.5.0/xdive/client.py +512 -0
- xdive-1.5.0/xdive/langchain.py +149 -0
- xdive-1.5.0/xdive/llama_index.py +38 -0
- xdive-1.5.0/xdive/py.typed +0 -0
xdive-1.5.0/.gitignore
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Binaries
|
|
2
|
+
/crawler
|
|
3
|
+
/server
|
|
4
|
+
/benchmark
|
|
5
|
+
/bin/
|
|
6
|
+
*.exe
|
|
7
|
+
|
|
8
|
+
# Crawl output
|
|
9
|
+
/out/
|
|
10
|
+
/benchmark-results/
|
|
11
|
+
/backups/
|
|
12
|
+
|
|
13
|
+
# SQLite database files
|
|
14
|
+
*.db
|
|
15
|
+
*.db-shm
|
|
16
|
+
*.db-wal
|
|
17
|
+
|
|
18
|
+
# Go
|
|
19
|
+
*.test
|
|
20
|
+
*.out
|
|
21
|
+
vendor/
|
|
22
|
+
|
|
23
|
+
# Local env files (secrets)
|
|
24
|
+
.env
|
|
25
|
+
web/.env
|
|
26
|
+
|
|
27
|
+
# Editor / tooling output
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
30
|
+
graphify-out/
|
|
31
|
+
.DS_Store
|
|
32
|
+
|
|
33
|
+
# Frontend build output
|
|
34
|
+
web/dist/
|
|
35
|
+
web/node_modules/
|
|
36
|
+
web/test-results/
|
|
37
|
+
web/playwright-report/
|
|
38
|
+
|
|
39
|
+
# Local Claude Code tooling (preview launch config)
|
|
40
|
+
.claude/
|
|
41
|
+
loadtest/results/
|
|
42
|
+
# SDK build artifacts
|
|
43
|
+
sdk/js/node_modules/
|
|
44
|
+
sdk/js/dist/
|
|
45
|
+
sdk/vite/node_modules/
|
|
46
|
+
sdk/vite/dist/
|
|
47
|
+
sdk/ruby/.bundle/
|
|
48
|
+
sdk/ruby/Gemfile.lock
|
|
49
|
+
sdk/ruby/vendor/
|
|
50
|
+
sdk/ruby/*.gem
|
|
51
|
+
sdk/java/target/
|
|
52
|
+
sdk/java/.idea/
|
|
53
|
+
sdk/java/*.iml
|
|
54
|
+
sdk/java/.gradle/
|
|
55
|
+
sdk/java/build/
|
|
56
|
+
sdk/php/vendor/
|
|
57
|
+
sdk/php/composer.lock
|
|
58
|
+
sdk/php/.phpunit.cache/
|
|
59
|
+
sdk/php/.phpstan.cache/
|
|
60
|
+
sdk/php/.php-cs-fixer.cache
|
|
61
|
+
sdk/rust/target/
|
|
62
|
+
sdk/rust/Cargo.lock
|
|
63
|
+
sdk/dotnet/**/bin/
|
|
64
|
+
sdk/dotnet/**/obj/
|
|
65
|
+
sdk/dotnet/out/
|
|
66
|
+
sdk/dotnet/**/*.user
|
|
67
|
+
sdk/dotnet/.vs/
|
|
68
|
+
sdk/dotnet/**/TestResults/
|
|
69
|
+
sdk/python/build/
|
|
70
|
+
sdk/python/dist/
|
|
71
|
+
sdk/python/*.egg-info/
|
|
72
|
+
sdk/python/.pytest_cache/
|
|
73
|
+
__pycache__/
|
|
74
|
+
# Head-to-head comparison output and credentials
|
|
75
|
+
arena-results/
|
|
76
|
+
.env.arena
|
xdive-1.5.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `xdive` Python package. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the package uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## Unreleased
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **The package is now `xdive`** (it was `crawler-sdk`, a name that belongs to
|
|
12
|
+
an unrelated PyPI project). Import it as `xdive`: the clients are `XDive` and
|
|
13
|
+
`AsyncXDive`, errors derive from `XDiveError`, and the integrations are
|
|
14
|
+
`XDiveSearchTool`, `XDiveScrapeTool`, `XDiveResearchTool`, `XDiveLoader` and
|
|
15
|
+
`XDiveReader`. The tools are named `xdive_search`, `xdive_scrape` and
|
|
16
|
+
`xdive_research`.
|
|
17
|
+
- The environment variables are `XDIVE_API_KEY` and `XDIVE_BASE_URL`. The
|
|
18
|
+
older `CRAWLER_API_KEY` and `CRAWLER_BASE_URL` still work as fallbacks.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Every `POST` carries an `Idempotency-Key`, reused by all retries of that
|
|
23
|
+
request, so a retry never runs or bills the work twice.
|
|
24
|
+
- Timeouts and network failures are retried (up to the retry limit) for `GET`
|
|
25
|
+
and `POST`, as is `409 IDEMPOTENCY_KEY_IN_PROGRESS`.
|
|
26
|
+
- `XDiveError.retryable` exposes the server's `retryable` flag.
|
|
27
|
+
- `AsyncXDive.batch_scrape`, `AsyncXDive.llms_txt` and
|
|
28
|
+
`AsyncXDive.crawl_errors`, with the same signatures as the synchronous
|
|
29
|
+
`XDive` methods. `AsyncXDive` now has every method `XDive` has.
|
|
30
|
+
- `page_markdown(page)`, exported from `xdive`: a page's Markdown from
|
|
31
|
+
`artifacts.markdown.content` or `markdown`, whichever carries it. The
|
|
32
|
+
LangChain and LlamaIndex integrations use it.
|
|
33
|
+
- A `py.typed` marker, so type checkers use the package's annotations, and
|
|
34
|
+
the `Typing :: Typed` classifier. `scrape(formats=...)` is annotated to
|
|
35
|
+
accept format dicts as well as names.
|
|
36
|
+
- The `research` docstring describes `usage.plan` (`single`, `multi_hop` or
|
|
37
|
+
`unavailable`).
|
|
38
|
+
- `LICENSE` (MIT) in the package, and a `Documentation` project URL.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Only overload is retried: a `429`, or a `503` whose body has the code
|
|
43
|
+
`SERVER_BUSY`. Any other `503` (for example "search is not configured" or
|
|
44
|
+
"could not enqueue crawl") now raises `XDiveError` at once with the API's
|
|
45
|
+
status, message and code, instead of being retried and raised as
|
|
46
|
+
`RateLimited`.
|
|
47
|
+
- The `search` docstrings no longer say that `search_depth="advanced"`
|
|
48
|
+
re-ranks results. It reads the top results and attaches each one's
|
|
49
|
+
query-relevant `passages` and a `relevance_score`; results keep the search
|
|
50
|
+
engine's order.
|
|
51
|
+
|
|
52
|
+
## 1.5.0
|
|
53
|
+
|
|
54
|
+
First packaged release.
|
|
55
|
+
|
|
56
|
+
- `XDive` (synchronous, `httpx.Client`) and `AsyncXDive` (asynchronous,
|
|
57
|
+
`httpx.AsyncClient`) clients, both usable as context managers.
|
|
58
|
+
- Configuration from arguments or the environment: `api_key` /
|
|
59
|
+
`CRAWLER_API_KEY`, `base_url` / `CRAWLER_BASE_URL`, plus `org_id`,
|
|
60
|
+
`timeout` and `max_retries`. The organization id is discovered once through
|
|
61
|
+
`GET /whoami`.
|
|
62
|
+
- `XDive` methods: `whoami`, `scrape`, `start_crawl`, `get_crawl`,
|
|
63
|
+
`cancel_crawl`, `crawl_errors`, `wait_for_crawl`, `crawl`, `batch_scrape`,
|
|
64
|
+
`map`, `search`, `research`, `extract` and `llms_txt`. `AsyncXDive` had
|
|
65
|
+
all of them as coroutines except `batch_scrape`, `llms_txt` and
|
|
66
|
+
`crawl_errors`.
|
|
67
|
+
- Automatic retries of `429` and `503` responses after the server's
|
|
68
|
+
`Retry-After`, up to `max_retries` (default 3).
|
|
69
|
+
- Crawl polling with `If-None-Match`, so unchanged status answers are bodiless
|
|
70
|
+
`304`s, at the interval the server suggests.
|
|
71
|
+
- Typed errors: `XDiveError` (`status`, `message`, `code`), `RateLimited`
|
|
72
|
+
(`retry_after`), `QuotaExceeded` (HTTP 402) and `CrawlTimeout`.
|
|
73
|
+
- LangChain integration (`xdive[langchain]`): `XDiveSearchTool`,
|
|
74
|
+
`XDiveScrapeTool`, `XDiveResearchTool` and `XDiveLoader`.
|
|
75
|
+
- LlamaIndex integration (`xdive[llama-index]`): `XDiveReader`.
|
xdive-1.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mehdi Sadeghian
|
|
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.
|
xdive-1.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: xdive
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Python SDK for XDive: scrape, crawl, map, search and research the web with citations, for AI agents.
|
|
5
|
+
Project-URL: Homepage, https://crawler.mehdisadeghian.com
|
|
6
|
+
Project-URL: Documentation, https://crawler.mehdisadeghian.com/docs/sdk-python
|
|
7
|
+
Author: XDive
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,crawler,langchain,llm,markdown,mcp,rag,research,search,web scraping,xdive
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: httpx<1,>=0.25
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: langchain-core>=0.2; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
23
|
+
Provides-Extra: langchain
|
|
24
|
+
Requires-Dist: langchain-core>=0.2; extra == 'langchain'
|
|
25
|
+
Provides-Extra: llama-index
|
|
26
|
+
Requires-Dist: llama-index-core>=0.10; extra == 'llama-index'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# xdive
|
|
30
|
+
|
|
31
|
+
Python client for [XDive](https://crawler.mehdisadeghian.com): scrape pages
|
|
32
|
+
to Markdown, crawl and map sites, search the web, and answer questions with
|
|
33
|
+
verified citations. Synchronous and asynchronous clients, typed errors,
|
|
34
|
+
automatic retries, and LangChain and LlamaIndex integrations.
|
|
35
|
+
|
|
36
|
+
Full documentation: https://crawler.mehdisadeghian.com/docs/sdk-python
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install xdive
|
|
42
|
+
pip install "xdive[langchain]" # LangChain tools and document loader
|
|
43
|
+
pip install "xdive[llama-index]" # LlamaIndex reader
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires Python 3.9 or later. The only runtime dependency is `httpx`.
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from xdive import XDive, page_markdown
|
|
52
|
+
|
|
53
|
+
with XDive() as xdive: # reads XDIVE_API_KEY
|
|
54
|
+
page = xdive.scrape("https://example.com", formats=["markdown", "links"])
|
|
55
|
+
print(page_markdown(page))
|
|
56
|
+
site = xdive.crawl("https://docs.example.com", max_pages=50) # waits for the crawl
|
|
57
|
+
answer = xdive.research("What does PEP 639 change about license metadata?")
|
|
58
|
+
print(answer["answer"])
|
|
59
|
+
for citation in answer["citations"]:
|
|
60
|
+
print(citation["number"], citation["url"])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Create an API key under **API Keys** in the dashboard. A key belongs to one
|
|
64
|
+
organization; the client looks the organization up once through
|
|
65
|
+
`GET /whoami`, so the key is all you configure.
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
| Argument | Environment variable | Default | Description |
|
|
70
|
+
|---|---|---|---|
|
|
71
|
+
| `api_key` | `XDIVE_API_KEY` (or `CRAWLER_API_KEY`) | (required) | Organization API key (`ck_live_…`). |
|
|
72
|
+
| `base_url` | `XDIVE_BASE_URL` (or `CRAWLER_BASE_URL`) | `https://crawler.mehdisadeghian.com/api` | API base URL, including `/api`. |
|
|
73
|
+
| `org_id` | | discovered | Skip the `/whoami` lookup when you know the organization id. |
|
|
74
|
+
| `timeout` | | `120.0` | Per-request timeout in seconds. |
|
|
75
|
+
| `max_retries` | | `3` | Retries of overload responses: `429`, and `503` with code `SERVER_BUSY`. |
|
|
76
|
+
|
|
77
|
+
A missing key raises `ValueError` when the client is created.
|
|
78
|
+
|
|
79
|
+
## Methods
|
|
80
|
+
|
|
81
|
+
`XDive` and `AsyncXDive` have the same methods; on `AsyncXDive` each is
|
|
82
|
+
a coroutine. Keyword options are passed through as the API's JSON fields.
|
|
83
|
+
|
|
84
|
+
| Method | API call | Returns |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `whoami()` | `GET /whoami` | The key's organization, scopes and plan. |
|
|
87
|
+
| `scrape(url, formats=None, **options)` | `POST /orgs/{id}/scrape` | The page object (`data`). |
|
|
88
|
+
| `crawl(url, *, timeout=600, poll_interval=2, **options)` | `POST /orgs/{id}/crawl`, then polls | The finished crawl record with `results`. |
|
|
89
|
+
| `start_crawl(url, **options)` | `POST /orgs/{id}/crawl` | The crawl id. |
|
|
90
|
+
| `get_crawl(crawl_id)` | `GET /orgs/{id}/crawls/{crawlId}` | The crawl record. |
|
|
91
|
+
| `wait_for_crawl(crawl_id, *, timeout=600, poll_interval=2)` | polls `GET /orgs/{id}/crawls/{crawlId}` | The finished crawl record. |
|
|
92
|
+
| `cancel_crawl(crawl_id)` | `DELETE /orgs/{id}/crawls/{crawlId}` | `None`. |
|
|
93
|
+
| `crawl_errors(crawl_id)` | `GET /orgs/{id}/crawls/{crawlId}/errors` | Failed pages (`errors`) and `robots_blocked` URLs. |
|
|
94
|
+
| `batch_scrape(urls, *, wait=True, timeout=600, **options)` | `POST /orgs/{id}/batch/scrape` | The finished record, or the job id with `wait=False`. |
|
|
95
|
+
| `map(url, *, search=None, limit=None, max_depth=None)` | `POST /orgs/{id}/map` | A list of URLs. |
|
|
96
|
+
| `search(query, **options)` | `POST /orgs/{id}/search` | The search response with `results`. |
|
|
97
|
+
| `research(query, *, depth="quick", output=None, schema=None, **options)` | `POST /orgs/{id}/research` | `answer` or `data`, `citations`, `sources`, `confidence`, `usage`. |
|
|
98
|
+
| `extract(urls, *, prompt=None, schema=None, **options)` | `POST /orgs/{id}/extract` | The extracted data. |
|
|
99
|
+
| `llms_txt(url, *, max_urls=None, full_text=False)` | `POST /orgs/{id}/llmstxt` | `llms_txt` (and `llms_full_txt`). |
|
|
100
|
+
|
|
101
|
+
`XDive.org_id` is a property; on `AsyncXDive` it is a coroutine,
|
|
102
|
+
`await client.org_id()`.
|
|
103
|
+
|
|
104
|
+
`formats` takes the API's format names (`markdown`, `text`, `html`,
|
|
105
|
+
`rawHtml`, `links`, `images`, `json`, `summary`, `question`, `highlights`,
|
|
106
|
+
`branding`, `audio`, `screenshot`, `product`, `menu`) or dicts such as
|
|
107
|
+
`{"type": "json", "schema": {...}}`. `page_markdown(page)` returns a page's
|
|
108
|
+
Markdown whichever field carries it (`artifacts.markdown.content` or
|
|
109
|
+
`markdown`).
|
|
110
|
+
|
|
111
|
+
## Async
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import asyncio
|
|
115
|
+
from xdive import AsyncXDive
|
|
116
|
+
|
|
117
|
+
async def main():
|
|
118
|
+
async with AsyncXDive() as client:
|
|
119
|
+
pages = await asyncio.gather(*(client.scrape(u) for u in ["https://a.example", "https://b.example"]))
|
|
120
|
+
|
|
121
|
+
asyncio.run(main())
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Close clients you do not use as context managers: `xdive.close()` or
|
|
125
|
+
`await client.aclose()`.
|
|
126
|
+
|
|
127
|
+
## Errors and retries
|
|
128
|
+
|
|
129
|
+
| Exception | When | Attributes |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `XDiveError` | Any API error (status 400 and above). Base class of the others. | `status`, `message`, `code` |
|
|
132
|
+
| `RateLimited` | Still overloaded (`429`, or `503` with code `SERVER_BUSY`) after `max_retries` retries. | `retry_after` (seconds) |
|
|
133
|
+
| `QuotaExceeded` | `402`: the organization's monthly quota is used up. | |
|
|
134
|
+
| `CrawlTimeout` | `crawl`, `wait_for_crawl` or `batch_scrape` waited longer than `timeout`. The job keeps running on the server. | `crawl_id` |
|
|
135
|
+
|
|
136
|
+
Only overload is retried: a `429` (organization rate or concurrency limit)
|
|
137
|
+
and a `503` whose JSON body has the code `SERVER_BUSY` (server at capacity).
|
|
138
|
+
The client waits the number of seconds in the response's `Retry-After`
|
|
139
|
+
header (1 second when absent) before each retry. Every other error, including
|
|
140
|
+
any other `503` (for example a feature the server has not configured), raises
|
|
141
|
+
`XDiveError` at once with the API's `status`, `message` and `code`. Network
|
|
142
|
+
errors and timeouts surface as `httpx` exceptions.
|
|
143
|
+
|
|
144
|
+
Crawl polling sends `If-None-Match`, so an unchanged status comes back as an
|
|
145
|
+
empty `304`, and waits the interval the server sets in `Retry-After`,
|
|
146
|
+
falling back to `poll_interval`.
|
|
147
|
+
|
|
148
|
+
## Integrations
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from xdive.langchain import XDiveLoader, XDiveResearchTool, XDiveScrapeTool, XDiveSearchTool
|
|
152
|
+
|
|
153
|
+
tools = [XDiveSearchTool(), XDiveScrapeTool(), XDiveResearchTool()]
|
|
154
|
+
docs = XDiveLoader("https://docs.example.com", mode="crawl", max_pages=100).load()
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from xdive.llama_index import XDiveReader
|
|
159
|
+
|
|
160
|
+
documents = XDiveReader().load_data("https://docs.example.com", mode="crawl", max_pages=100)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`XDiveSearchTool` returns each result's query-relevant passages rather than
|
|
164
|
+
whole pages, which keeps an agent's context small. See the
|
|
165
|
+
[LangChain](https://crawler.mehdisadeghian.com/docs/langchain) and
|
|
166
|
+
[LlamaIndex](https://crawler.mehdisadeghian.com/docs/llamaindex) guides.
|
|
167
|
+
|
|
168
|
+
## Self-hosted
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
xdive = XDive(api_key="ck_live_…", base_url="https://your-host/api")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
or set `XDIVE_BASE_URL=https://your-host/api`.
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pip install -e ".[dev]"
|
|
180
|
+
pytest
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT. The license text is in the `LICENSE` file included in the package.
|
xdive-1.5.0/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# xdive
|
|
2
|
+
|
|
3
|
+
Python client for [XDive](https://crawler.mehdisadeghian.com): scrape pages
|
|
4
|
+
to Markdown, crawl and map sites, search the web, and answer questions with
|
|
5
|
+
verified citations. Synchronous and asynchronous clients, typed errors,
|
|
6
|
+
automatic retries, and LangChain and LlamaIndex integrations.
|
|
7
|
+
|
|
8
|
+
Full documentation: https://crawler.mehdisadeghian.com/docs/sdk-python
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install xdive
|
|
14
|
+
pip install "xdive[langchain]" # LangChain tools and document loader
|
|
15
|
+
pip install "xdive[llama-index]" # LlamaIndex reader
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Python 3.9 or later. The only runtime dependency is `httpx`.
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from xdive import XDive, page_markdown
|
|
24
|
+
|
|
25
|
+
with XDive() as xdive: # reads XDIVE_API_KEY
|
|
26
|
+
page = xdive.scrape("https://example.com", formats=["markdown", "links"])
|
|
27
|
+
print(page_markdown(page))
|
|
28
|
+
site = xdive.crawl("https://docs.example.com", max_pages=50) # waits for the crawl
|
|
29
|
+
answer = xdive.research("What does PEP 639 change about license metadata?")
|
|
30
|
+
print(answer["answer"])
|
|
31
|
+
for citation in answer["citations"]:
|
|
32
|
+
print(citation["number"], citation["url"])
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Create an API key under **API Keys** in the dashboard. A key belongs to one
|
|
36
|
+
organization; the client looks the organization up once through
|
|
37
|
+
`GET /whoami`, so the key is all you configure.
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
| Argument | Environment variable | Default | Description |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| `api_key` | `XDIVE_API_KEY` (or `CRAWLER_API_KEY`) | (required) | Organization API key (`ck_live_…`). |
|
|
44
|
+
| `base_url` | `XDIVE_BASE_URL` (or `CRAWLER_BASE_URL`) | `https://crawler.mehdisadeghian.com/api` | API base URL, including `/api`. |
|
|
45
|
+
| `org_id` | | discovered | Skip the `/whoami` lookup when you know the organization id. |
|
|
46
|
+
| `timeout` | | `120.0` | Per-request timeout in seconds. |
|
|
47
|
+
| `max_retries` | | `3` | Retries of overload responses: `429`, and `503` with code `SERVER_BUSY`. |
|
|
48
|
+
|
|
49
|
+
A missing key raises `ValueError` when the client is created.
|
|
50
|
+
|
|
51
|
+
## Methods
|
|
52
|
+
|
|
53
|
+
`XDive` and `AsyncXDive` have the same methods; on `AsyncXDive` each is
|
|
54
|
+
a coroutine. Keyword options are passed through as the API's JSON fields.
|
|
55
|
+
|
|
56
|
+
| Method | API call | Returns |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `whoami()` | `GET /whoami` | The key's organization, scopes and plan. |
|
|
59
|
+
| `scrape(url, formats=None, **options)` | `POST /orgs/{id}/scrape` | The page object (`data`). |
|
|
60
|
+
| `crawl(url, *, timeout=600, poll_interval=2, **options)` | `POST /orgs/{id}/crawl`, then polls | The finished crawl record with `results`. |
|
|
61
|
+
| `start_crawl(url, **options)` | `POST /orgs/{id}/crawl` | The crawl id. |
|
|
62
|
+
| `get_crawl(crawl_id)` | `GET /orgs/{id}/crawls/{crawlId}` | The crawl record. |
|
|
63
|
+
| `wait_for_crawl(crawl_id, *, timeout=600, poll_interval=2)` | polls `GET /orgs/{id}/crawls/{crawlId}` | The finished crawl record. |
|
|
64
|
+
| `cancel_crawl(crawl_id)` | `DELETE /orgs/{id}/crawls/{crawlId}` | `None`. |
|
|
65
|
+
| `crawl_errors(crawl_id)` | `GET /orgs/{id}/crawls/{crawlId}/errors` | Failed pages (`errors`) and `robots_blocked` URLs. |
|
|
66
|
+
| `batch_scrape(urls, *, wait=True, timeout=600, **options)` | `POST /orgs/{id}/batch/scrape` | The finished record, or the job id with `wait=False`. |
|
|
67
|
+
| `map(url, *, search=None, limit=None, max_depth=None)` | `POST /orgs/{id}/map` | A list of URLs. |
|
|
68
|
+
| `search(query, **options)` | `POST /orgs/{id}/search` | The search response with `results`. |
|
|
69
|
+
| `research(query, *, depth="quick", output=None, schema=None, **options)` | `POST /orgs/{id}/research` | `answer` or `data`, `citations`, `sources`, `confidence`, `usage`. |
|
|
70
|
+
| `extract(urls, *, prompt=None, schema=None, **options)` | `POST /orgs/{id}/extract` | The extracted data. |
|
|
71
|
+
| `llms_txt(url, *, max_urls=None, full_text=False)` | `POST /orgs/{id}/llmstxt` | `llms_txt` (and `llms_full_txt`). |
|
|
72
|
+
|
|
73
|
+
`XDive.org_id` is a property; on `AsyncXDive` it is a coroutine,
|
|
74
|
+
`await client.org_id()`.
|
|
75
|
+
|
|
76
|
+
`formats` takes the API's format names (`markdown`, `text`, `html`,
|
|
77
|
+
`rawHtml`, `links`, `images`, `json`, `summary`, `question`, `highlights`,
|
|
78
|
+
`branding`, `audio`, `screenshot`, `product`, `menu`) or dicts such as
|
|
79
|
+
`{"type": "json", "schema": {...}}`. `page_markdown(page)` returns a page's
|
|
80
|
+
Markdown whichever field carries it (`artifacts.markdown.content` or
|
|
81
|
+
`markdown`).
|
|
82
|
+
|
|
83
|
+
## Async
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import asyncio
|
|
87
|
+
from xdive import AsyncXDive
|
|
88
|
+
|
|
89
|
+
async def main():
|
|
90
|
+
async with AsyncXDive() as client:
|
|
91
|
+
pages = await asyncio.gather(*(client.scrape(u) for u in ["https://a.example", "https://b.example"]))
|
|
92
|
+
|
|
93
|
+
asyncio.run(main())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Close clients you do not use as context managers: `xdive.close()` or
|
|
97
|
+
`await client.aclose()`.
|
|
98
|
+
|
|
99
|
+
## Errors and retries
|
|
100
|
+
|
|
101
|
+
| Exception | When | Attributes |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `XDiveError` | Any API error (status 400 and above). Base class of the others. | `status`, `message`, `code` |
|
|
104
|
+
| `RateLimited` | Still overloaded (`429`, or `503` with code `SERVER_BUSY`) after `max_retries` retries. | `retry_after` (seconds) |
|
|
105
|
+
| `QuotaExceeded` | `402`: the organization's monthly quota is used up. | |
|
|
106
|
+
| `CrawlTimeout` | `crawl`, `wait_for_crawl` or `batch_scrape` waited longer than `timeout`. The job keeps running on the server. | `crawl_id` |
|
|
107
|
+
|
|
108
|
+
Only overload is retried: a `429` (organization rate or concurrency limit)
|
|
109
|
+
and a `503` whose JSON body has the code `SERVER_BUSY` (server at capacity).
|
|
110
|
+
The client waits the number of seconds in the response's `Retry-After`
|
|
111
|
+
header (1 second when absent) before each retry. Every other error, including
|
|
112
|
+
any other `503` (for example a feature the server has not configured), raises
|
|
113
|
+
`XDiveError` at once with the API's `status`, `message` and `code`. Network
|
|
114
|
+
errors and timeouts surface as `httpx` exceptions.
|
|
115
|
+
|
|
116
|
+
Crawl polling sends `If-None-Match`, so an unchanged status comes back as an
|
|
117
|
+
empty `304`, and waits the interval the server sets in `Retry-After`,
|
|
118
|
+
falling back to `poll_interval`.
|
|
119
|
+
|
|
120
|
+
## Integrations
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from xdive.langchain import XDiveLoader, XDiveResearchTool, XDiveScrapeTool, XDiveSearchTool
|
|
124
|
+
|
|
125
|
+
tools = [XDiveSearchTool(), XDiveScrapeTool(), XDiveResearchTool()]
|
|
126
|
+
docs = XDiveLoader("https://docs.example.com", mode="crawl", max_pages=100).load()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from xdive.llama_index import XDiveReader
|
|
131
|
+
|
|
132
|
+
documents = XDiveReader().load_data("https://docs.example.com", mode="crawl", max_pages=100)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`XDiveSearchTool` returns each result's query-relevant passages rather than
|
|
136
|
+
whole pages, which keeps an agent's context small. See the
|
|
137
|
+
[LangChain](https://crawler.mehdisadeghian.com/docs/langchain) and
|
|
138
|
+
[LlamaIndex](https://crawler.mehdisadeghian.com/docs/llamaindex) guides.
|
|
139
|
+
|
|
140
|
+
## Self-hosted
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
xdive = XDive(api_key="ck_live_…", base_url="https://your-host/api")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
or set `XDIVE_BASE_URL=https://your-host/api`.
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pip install -e ".[dev]"
|
|
152
|
+
pytest
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT. The license text is in the `LICENSE` file included in the package.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xdive"
|
|
7
|
+
version = "1.5.0"
|
|
8
|
+
description = "Python SDK for XDive: scrape, crawl, map, search and research the web with citations, for AI agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "XDive" }]
|
|
13
|
+
keywords = ["xdive", "web scraping", "crawler", "markdown", "rag", "search", "research", "llm", "agents", "langchain", "mcp"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = ["httpx>=0.25,<1"]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
langchain = ["langchain-core>=0.2"]
|
|
27
|
+
llama-index = ["llama-index-core>=0.10"]
|
|
28
|
+
dev = ["pytest>=8", "langchain-core>=0.2"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://crawler.mehdisadeghian.com"
|
|
32
|
+
Documentation = "https://crawler.mehdisadeghian.com/docs/sdk-python"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["xdive"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|