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 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
@@ -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"]