hydrafetch 0.1.0__tar.gz → 0.2.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.
@@ -11,7 +11,8 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  environment: pypi
13
13
  permissions:
14
- contents: read
14
+ # contents: write is what lets the release be created; the publish itself needs only read.
15
+ contents: write
15
16
  id-token: write
16
17
  steps:
17
18
  - uses: actions/checkout@v4
@@ -25,3 +26,10 @@ jobs:
25
26
  [ "$TAG" = "$PKG" ] || { echo "tag $TAG != pyproject $PKG"; exit 1; }
26
27
  - run: uv build
27
28
  - uses: pypa/gh-action-pypi-publish@release/v1
29
+
30
+ # After the publish, never before: a release pointing at a version that is not on PyPI is
31
+ # worse than no release at all.
32
+ - name: Create the GitHub release
33
+ uses: softprops/action-gh-release@v2
34
+ with:
35
+ generate_release_notes: true
@@ -0,0 +1,286 @@
1
+ Metadata-Version: 2.5
2
+ Name: hydrafetch
3
+ Version: 0.2.0
4
+ Summary: Official Python client for the Hydrafetch web data API. Turn any URL into clean Markdown and structured data.
5
+ Project-URL: Homepage, https://hydrafetch.com
6
+ Project-URL: Documentation, https://docs.hydrafetch.com
7
+ Project-URL: Source, https://github.com/Hydrafetch/python-sdk
8
+ Project-URL: Issues, https://github.com/Hydrafetch/python-sdk/issues
9
+ Author-email: Hydrafetch <team@hydrafetch.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,crawler,hydrafetch,llm,markdown,rag,structured-data,web-scraping
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: httpx>=0.24
22
+ Description-Content-Type: text/markdown
23
+
24
+ # hydrafetch
25
+
26
+ [![PyPI](https://img.shields.io/pypi/v/hydrafetch)](https://pypi.org/project/hydrafetch/)
27
+ [![CI](https://github.com/Hydrafetch/python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/Hydrafetch/python-sdk/actions/workflows/ci.yml)
28
+ [![Python](https://img.shields.io/pypi/pyversions/hydrafetch)](https://pypi.org/project/hydrafetch/)
29
+
30
+ Official Python client for the [Hydrafetch](https://hydrafetch.com) web data API.
31
+
32
+ Turn any URL into clean Markdown or schema-shaped JSON. Sync and async, fully typed, one dependency.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install hydrafetch
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ from hydrafetch import Hydrafetch
44
+
45
+ hf = Hydrafetch()
46
+
47
+ page = hf.scrape("https://example.com/article")
48
+ print(page["markdown"])
49
+ ```
50
+
51
+ Create a key at [app.hydrafetch.com](https://app.hydrafetch.com). The constructor reads `HYDRAFETCH_API_KEY` when no key is passed.
52
+
53
+ Both clients are context managers, which closes the connection pool deterministically:
54
+
55
+ ```python
56
+ with Hydrafetch() as hf:
57
+ page = hf.scrape("https://example.com")
58
+ ```
59
+
60
+ ## Scraping
61
+
62
+ ```python
63
+ page = hf.scrape(
64
+ "https://example.com/article",
65
+ formats=["markdown", "links"],
66
+ onlyMainContent=True,
67
+ preferStructure=True,
68
+ blockAds=True,
69
+ maxAge=3_600_000,
70
+ )
71
+ ```
72
+
73
+ Option names are camelCase because they are passed to the API unchanged. Client arguments such as `api_key`, `max_retries`, `poll_interval` and `on_progress` are snake_case.
74
+
75
+ | Format | Key | Contains |
76
+ | --- | --- | --- |
77
+ | `markdown` | `markdown` | clean Markdown, the default |
78
+ | `html` | `html` | rendered HTML |
79
+ | `rawHtml` | `rawHtml` | the untouched response body |
80
+ | `links` | `links` | every link on the page |
81
+ | `structured` | `structured` | the page's own JSON-LD and microdata |
82
+ | `summary` | `summary` | a short summary |
83
+ | `json` | `json` | schema-shaped JSON, see `jsonOptions` |
84
+ | `brand` | `brand` | the site's brand record |
85
+
86
+ `hf.markdown(url)` returns the Markdown string directly.
87
+
88
+ ## Structured extraction
89
+
90
+ ```python
91
+ out = hf.extract(
92
+ ["https://example.com/product/1", "https://example.com/product/2"],
93
+ schema={
94
+ "type": "object",
95
+ "properties": {
96
+ "name": {"type": "string"},
97
+ "price_usd": {"type": "number"},
98
+ },
99
+ },
100
+ )
101
+
102
+ for item in out["results"]:
103
+ print(item["url"], (item.get("data") or {}).get("name"))
104
+ ```
105
+
106
+ Pass `prompt=` instead of, or alongside, `schema=` to describe the fields in plain language.
107
+
108
+ ### From a pydantic model
109
+
110
+ `schema=` also takes a pydantic model, so the shape you already parse into is the shape you ask for. A `Field` description is carried through and tells the model what the field means, which is worth more on an ambiguous page than the field name alone.
111
+
112
+ ```python
113
+ from pydantic import BaseModel, Field
114
+
115
+ class Product(BaseModel):
116
+ name: str = Field(description="The product being sold on this page, not a recommended one")
117
+ price_usd: float = Field(description="Current price in USD, after any discount")
118
+
119
+ out = hf.extract("https://example.com/product/1", schema=Product)
120
+ ```
121
+
122
+ Nested models work and are flattened before sending. pydantic is not a dependency and is imported only if you pass a model. A model that refers to itself cannot be flattened and is refused rather than sent.
123
+
124
+ ## Discovery and bulk work
125
+
126
+ `map` lists a site's URLs for one credit without fetching any page.
127
+
128
+ ```python
129
+ links = hf.map("https://example.com", limit=1000)["links"]
130
+ docs = [url for url in links if "/docs/" in url]
131
+ ```
132
+
133
+ `batch` and `crawl` submit a job and poll until it finishes.
134
+
135
+ ```python
136
+ job = hf.batch(
137
+ docs,
138
+ scrapeOptions={"formats": ["markdown"]},
139
+ on_progress=lambda j: print(j["status"], j.get("completed"), "/", j.get("total")),
140
+ )
141
+
142
+ for page in job.get("pages", []):
143
+ print(page["url"], len((page.get("data") or {}).get("markdown") or ""))
144
+ ```
145
+
146
+ Pass a `webhook` and use `start_crawl` or `start_batch` to return immediately instead of polling.
147
+
148
+ ```python
149
+ crawl_id = hf.start_crawl(
150
+ "https://example.com",
151
+ limit=500,
152
+ maxDepth=3,
153
+ includePaths=["/docs"],
154
+ webhook="https://your.app/hooks/hydrafetch",
155
+ )
156
+ ```
157
+
158
+ ## Search
159
+
160
+ ```python
161
+ res = hf.search("post-quantum TLS adoption", limit=5, scrapeResults=True)
162
+
163
+ for r in res["results"]:
164
+ print(r["title"], r["url"])
165
+ print(((r.get("data") or {}).get("markdown") or "")[:500])
166
+ ```
167
+
168
+ ## Brand data
169
+
170
+ ```python
171
+ hf.brand("stripe.com") # logos, colours, fonts, socials
172
+ hf.logo("stripe.com", theme="dark", type="icon") # one asset
173
+ hf.styleguide("stripe.com") # computed design system
174
+ ```
175
+
176
+ For logos in a browser use [`@hydrafetch/client-sdk`](https://github.com/Hydrafetch/client-sdk) with a publishable key. Those bill against logo pulls rather than credits.
177
+
178
+ ## Async
179
+
180
+ `AsyncHydrafetch` mirrors the same surface. Use it when several calls can run concurrently.
181
+
182
+ ```python
183
+ import asyncio
184
+ from hydrafetch import AsyncHydrafetch
185
+
186
+ async def main():
187
+ async with AsyncHydrafetch() as hf:
188
+ pages = await asyncio.gather(
189
+ hf.scrape("https://a.example"),
190
+ hf.scrape("https://b.example"),
191
+ )
192
+ return [page["markdown"] for page in pages]
193
+
194
+ asyncio.run(main())
195
+ ```
196
+
197
+ `start_crawl`, `crawl_status`, `start_batch` and `batch_status` are available on the async client. The polling helpers `crawl` and `batch` are sync only; on the async client, poll the status methods or use a webhook.
198
+
199
+ ## Error handling
200
+
201
+ All failures raise `HydrafetchError`, carrying the API's error code, HTTP status and request id.
202
+
203
+ ```python
204
+ from hydrafetch import HydrafetchError, HydrafetchTimeout
205
+
206
+ try:
207
+ page = hf.scrape(url)
208
+ except HydrafetchTimeout:
209
+ raise
210
+ except HydrafetchError as err:
211
+ if err.is_auth:
212
+ refresh_key()
213
+ elif err.is_out_of_credits:
214
+ top_up()
215
+ elif err.is_invalid_request:
216
+ report(err.message)
217
+ elif err.is_retryable:
218
+ enqueue(url)
219
+ else:
220
+ print(err.code, err.status, err.request_id)
221
+ raise
222
+ ```
223
+
224
+ | Status | Meaning | Retried |
225
+ | --- | --- | --- |
226
+ | 400, 422 | invalid request | no |
227
+ | 401, 403 | invalid or missing key | no |
228
+ | 402 | out of credits | no |
229
+ | 404 | page does not exist | no |
230
+ | 429 | rate limited | yes, twice with backoff |
231
+ | 5xx | upstream failure | yes, twice with backoff |
232
+
233
+ A 503 from `scrape` means the origin is unreachable, usually a dead domain or a broken certificate.
234
+
235
+ ## Configuration
236
+
237
+ ```python
238
+ hf = Hydrafetch(
239
+ api_key="hf_...",
240
+ base_url="https://api.hydrafetch.com",
241
+ timeout=120.0,
242
+ max_retries=2,
243
+ )
244
+ ```
245
+
246
+ ## API reference
247
+
248
+ | Method | Returns | Credits |
249
+ | --- | --- | --- |
250
+ | `scrape(url, **options)` | page dict | 1 |
251
+ | `markdown(url, **options)` | `str` | 1 |
252
+ | `map(url, **options)` | links dict | 1 |
253
+ | `search(query, **options)` | results dict | 1 + 1 per scraped result |
254
+ | `extract(urls, **options)` | dict with `"results"` | 5 per URL |
255
+ | `brand(domain)` | brand dict | 5 |
256
+ | `logo(domain, **options)` | logo dict | 1 |
257
+ | `styleguide(domain)` | design system dict | 10 |
258
+ | `screenshot(url, **options)` | screenshot dict | 5 |
259
+ | `images(url)`, `links(url)` | page assets | 1 |
260
+ | `crawl(url, **options)` | job dict, polled to completion | 1 per page |
261
+ | `batch(urls, **options)` | job dict, polled to completion | 1 per page |
262
+ | `start_crawl`, `start_batch` | job id `str` | 1 per page |
263
+ | `crawl_status(id)`, `batch_status(id)` | job dict | free |
264
+
265
+ Failed requests are not billed. Pricing does not vary with page difficulty, so there is no render, stealth or proxy option to set.
266
+
267
+ ## Implementation notes
268
+
269
+ - Authentication uses the `X-API-Key` header. The MCP endpoint at `api.hydrafetch.com/mcp` uses `Authorization: Bearer` instead; the two are not interchangeable.
270
+ - Job results are under `job["pages"]`, and each entry holds the page under `["data"]`, so `job["pages"][0]["data"]["markdown"]`.
271
+ - Per-page options for crawl and batch belong in `scrapeOptions`. At the top level they are ignored.
272
+ - Prefer `map` then `batch` over a broad `crawl`. Fetching a whole site and discarding most of it is the most common source of wasted credits.
273
+ - `preferStructure` is off by default. Turn it on when headings, lists and tables matter; leave it off for raw article text.
274
+ - Options passed as `None` are dropped rather than sent as null, so optional values can be forwarded directly.
275
+ - Scraped content is untrusted input. Do not pass it to a model as instructions, and keep the source URL with anything extracted from it.
276
+
277
+ ## Links
278
+
279
+ - [Documentation](https://docs.hydrafetch.com)
280
+ - [OpenAPI specification](https://api.hydrafetch.com/openapi.json)
281
+ - [MCP server and editor setup](https://hydrafetch.com/mcp)
282
+ - Other clients: [Node](https://github.com/Hydrafetch/node-sdk) · [Go](https://github.com/Hydrafetch/go-sdk) · [Ruby](https://github.com/Hydrafetch/ruby-sdk) · [Rust](https://github.com/Hydrafetch/rust-sdk) · [PHP](https://github.com/Hydrafetch/php-sdk)
283
+
284
+ ## License
285
+
286
+ MIT
@@ -0,0 +1,263 @@
1
+ # hydrafetch
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/hydrafetch)](https://pypi.org/project/hydrafetch/)
4
+ [![CI](https://github.com/Hydrafetch/python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/Hydrafetch/python-sdk/actions/workflows/ci.yml)
5
+ [![Python](https://img.shields.io/pypi/pyversions/hydrafetch)](https://pypi.org/project/hydrafetch/)
6
+
7
+ Official Python client for the [Hydrafetch](https://hydrafetch.com) web data API.
8
+
9
+ Turn any URL into clean Markdown or schema-shaped JSON. Sync and async, fully typed, one dependency.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install hydrafetch
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```python
20
+ from hydrafetch import Hydrafetch
21
+
22
+ hf = Hydrafetch()
23
+
24
+ page = hf.scrape("https://example.com/article")
25
+ print(page["markdown"])
26
+ ```
27
+
28
+ Create a key at [app.hydrafetch.com](https://app.hydrafetch.com). The constructor reads `HYDRAFETCH_API_KEY` when no key is passed.
29
+
30
+ Both clients are context managers, which closes the connection pool deterministically:
31
+
32
+ ```python
33
+ with Hydrafetch() as hf:
34
+ page = hf.scrape("https://example.com")
35
+ ```
36
+
37
+ ## Scraping
38
+
39
+ ```python
40
+ page = hf.scrape(
41
+ "https://example.com/article",
42
+ formats=["markdown", "links"],
43
+ onlyMainContent=True,
44
+ preferStructure=True,
45
+ blockAds=True,
46
+ maxAge=3_600_000,
47
+ )
48
+ ```
49
+
50
+ Option names are camelCase because they are passed to the API unchanged. Client arguments such as `api_key`, `max_retries`, `poll_interval` and `on_progress` are snake_case.
51
+
52
+ | Format | Key | Contains |
53
+ | --- | --- | --- |
54
+ | `markdown` | `markdown` | clean Markdown, the default |
55
+ | `html` | `html` | rendered HTML |
56
+ | `rawHtml` | `rawHtml` | the untouched response body |
57
+ | `links` | `links` | every link on the page |
58
+ | `structured` | `structured` | the page's own JSON-LD and microdata |
59
+ | `summary` | `summary` | a short summary |
60
+ | `json` | `json` | schema-shaped JSON, see `jsonOptions` |
61
+ | `brand` | `brand` | the site's brand record |
62
+
63
+ `hf.markdown(url)` returns the Markdown string directly.
64
+
65
+ ## Structured extraction
66
+
67
+ ```python
68
+ out = hf.extract(
69
+ ["https://example.com/product/1", "https://example.com/product/2"],
70
+ schema={
71
+ "type": "object",
72
+ "properties": {
73
+ "name": {"type": "string"},
74
+ "price_usd": {"type": "number"},
75
+ },
76
+ },
77
+ )
78
+
79
+ for item in out["results"]:
80
+ print(item["url"], (item.get("data") or {}).get("name"))
81
+ ```
82
+
83
+ Pass `prompt=` instead of, or alongside, `schema=` to describe the fields in plain language.
84
+
85
+ ### From a pydantic model
86
+
87
+ `schema=` also takes a pydantic model, so the shape you already parse into is the shape you ask for. A `Field` description is carried through and tells the model what the field means, which is worth more on an ambiguous page than the field name alone.
88
+
89
+ ```python
90
+ from pydantic import BaseModel, Field
91
+
92
+ class Product(BaseModel):
93
+ name: str = Field(description="The product being sold on this page, not a recommended one")
94
+ price_usd: float = Field(description="Current price in USD, after any discount")
95
+
96
+ out = hf.extract("https://example.com/product/1", schema=Product)
97
+ ```
98
+
99
+ Nested models work and are flattened before sending. pydantic is not a dependency and is imported only if you pass a model. A model that refers to itself cannot be flattened and is refused rather than sent.
100
+
101
+ ## Discovery and bulk work
102
+
103
+ `map` lists a site's URLs for one credit without fetching any page.
104
+
105
+ ```python
106
+ links = hf.map("https://example.com", limit=1000)["links"]
107
+ docs = [url for url in links if "/docs/" in url]
108
+ ```
109
+
110
+ `batch` and `crawl` submit a job and poll until it finishes.
111
+
112
+ ```python
113
+ job = hf.batch(
114
+ docs,
115
+ scrapeOptions={"formats": ["markdown"]},
116
+ on_progress=lambda j: print(j["status"], j.get("completed"), "/", j.get("total")),
117
+ )
118
+
119
+ for page in job.get("pages", []):
120
+ print(page["url"], len((page.get("data") or {}).get("markdown") or ""))
121
+ ```
122
+
123
+ Pass a `webhook` and use `start_crawl` or `start_batch` to return immediately instead of polling.
124
+
125
+ ```python
126
+ crawl_id = hf.start_crawl(
127
+ "https://example.com",
128
+ limit=500,
129
+ maxDepth=3,
130
+ includePaths=["/docs"],
131
+ webhook="https://your.app/hooks/hydrafetch",
132
+ )
133
+ ```
134
+
135
+ ## Search
136
+
137
+ ```python
138
+ res = hf.search("post-quantum TLS adoption", limit=5, scrapeResults=True)
139
+
140
+ for r in res["results"]:
141
+ print(r["title"], r["url"])
142
+ print(((r.get("data") or {}).get("markdown") or "")[:500])
143
+ ```
144
+
145
+ ## Brand data
146
+
147
+ ```python
148
+ hf.brand("stripe.com") # logos, colours, fonts, socials
149
+ hf.logo("stripe.com", theme="dark", type="icon") # one asset
150
+ hf.styleguide("stripe.com") # computed design system
151
+ ```
152
+
153
+ For logos in a browser use [`@hydrafetch/client-sdk`](https://github.com/Hydrafetch/client-sdk) with a publishable key. Those bill against logo pulls rather than credits.
154
+
155
+ ## Async
156
+
157
+ `AsyncHydrafetch` mirrors the same surface. Use it when several calls can run concurrently.
158
+
159
+ ```python
160
+ import asyncio
161
+ from hydrafetch import AsyncHydrafetch
162
+
163
+ async def main():
164
+ async with AsyncHydrafetch() as hf:
165
+ pages = await asyncio.gather(
166
+ hf.scrape("https://a.example"),
167
+ hf.scrape("https://b.example"),
168
+ )
169
+ return [page["markdown"] for page in pages]
170
+
171
+ asyncio.run(main())
172
+ ```
173
+
174
+ `start_crawl`, `crawl_status`, `start_batch` and `batch_status` are available on the async client. The polling helpers `crawl` and `batch` are sync only; on the async client, poll the status methods or use a webhook.
175
+
176
+ ## Error handling
177
+
178
+ All failures raise `HydrafetchError`, carrying the API's error code, HTTP status and request id.
179
+
180
+ ```python
181
+ from hydrafetch import HydrafetchError, HydrafetchTimeout
182
+
183
+ try:
184
+ page = hf.scrape(url)
185
+ except HydrafetchTimeout:
186
+ raise
187
+ except HydrafetchError as err:
188
+ if err.is_auth:
189
+ refresh_key()
190
+ elif err.is_out_of_credits:
191
+ top_up()
192
+ elif err.is_invalid_request:
193
+ report(err.message)
194
+ elif err.is_retryable:
195
+ enqueue(url)
196
+ else:
197
+ print(err.code, err.status, err.request_id)
198
+ raise
199
+ ```
200
+
201
+ | Status | Meaning | Retried |
202
+ | --- | --- | --- |
203
+ | 400, 422 | invalid request | no |
204
+ | 401, 403 | invalid or missing key | no |
205
+ | 402 | out of credits | no |
206
+ | 404 | page does not exist | no |
207
+ | 429 | rate limited | yes, twice with backoff |
208
+ | 5xx | upstream failure | yes, twice with backoff |
209
+
210
+ A 503 from `scrape` means the origin is unreachable, usually a dead domain or a broken certificate.
211
+
212
+ ## Configuration
213
+
214
+ ```python
215
+ hf = Hydrafetch(
216
+ api_key="hf_...",
217
+ base_url="https://api.hydrafetch.com",
218
+ timeout=120.0,
219
+ max_retries=2,
220
+ )
221
+ ```
222
+
223
+ ## API reference
224
+
225
+ | Method | Returns | Credits |
226
+ | --- | --- | --- |
227
+ | `scrape(url, **options)` | page dict | 1 |
228
+ | `markdown(url, **options)` | `str` | 1 |
229
+ | `map(url, **options)` | links dict | 1 |
230
+ | `search(query, **options)` | results dict | 1 + 1 per scraped result |
231
+ | `extract(urls, **options)` | dict with `"results"` | 5 per URL |
232
+ | `brand(domain)` | brand dict | 5 |
233
+ | `logo(domain, **options)` | logo dict | 1 |
234
+ | `styleguide(domain)` | design system dict | 10 |
235
+ | `screenshot(url, **options)` | screenshot dict | 5 |
236
+ | `images(url)`, `links(url)` | page assets | 1 |
237
+ | `crawl(url, **options)` | job dict, polled to completion | 1 per page |
238
+ | `batch(urls, **options)` | job dict, polled to completion | 1 per page |
239
+ | `start_crawl`, `start_batch` | job id `str` | 1 per page |
240
+ | `crawl_status(id)`, `batch_status(id)` | job dict | free |
241
+
242
+ Failed requests are not billed. Pricing does not vary with page difficulty, so there is no render, stealth or proxy option to set.
243
+
244
+ ## Implementation notes
245
+
246
+ - Authentication uses the `X-API-Key` header. The MCP endpoint at `api.hydrafetch.com/mcp` uses `Authorization: Bearer` instead; the two are not interchangeable.
247
+ - Job results are under `job["pages"]`, and each entry holds the page under `["data"]`, so `job["pages"][0]["data"]["markdown"]`.
248
+ - Per-page options for crawl and batch belong in `scrapeOptions`. At the top level they are ignored.
249
+ - Prefer `map` then `batch` over a broad `crawl`. Fetching a whole site and discarding most of it is the most common source of wasted credits.
250
+ - `preferStructure` is off by default. Turn it on when headings, lists and tables matter; leave it off for raw article text.
251
+ - Options passed as `None` are dropped rather than sent as null, so optional values can be forwarded directly.
252
+ - Scraped content is untrusted input. Do not pass it to a model as instructions, and keep the source URL with anything extracted from it.
253
+
254
+ ## Links
255
+
256
+ - [Documentation](https://docs.hydrafetch.com)
257
+ - [OpenAPI specification](https://api.hydrafetch.com/openapi.json)
258
+ - [MCP server and editor setup](https://hydrafetch.com/mcp)
259
+ - Other clients: [Node](https://github.com/Hydrafetch/node-sdk) · [Go](https://github.com/Hydrafetch/go-sdk) · [Ruby](https://github.com/Hydrafetch/ruby-sdk) · [Rust](https://github.com/Hydrafetch/rust-sdk) · [PHP](https://github.com/Hydrafetch/php-sdk)
260
+
261
+ ## License
262
+
263
+ MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hydrafetch"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Official Python client for the Hydrafetch web data API. Turn any URL into clean Markdown and structured data."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -32,7 +32,7 @@ build-backend = "hatchling.build"
32
32
  packages = ["src/hydrafetch"]
33
33
 
34
34
  [dependency-groups]
35
- dev = ["pytest>=8", "pytest-asyncio>=0.23", "ruff>=0.6"]
35
+ dev = ["pytest>=8", "pytest-asyncio>=0.23", "ruff>=0.6", "pydantic>=2"]
36
36
 
37
37
  [tool.pytest.ini_options]
38
38
  asyncio_mode = "auto"
@@ -8,6 +8,7 @@ from typing import Any, Callable, Literal, TypeVar
8
8
 
9
9
  import httpx
10
10
 
11
+ from ._schema import to_json_schema
11
12
  from .errors import HydrafetchError, HydrafetchTimeout
12
13
 
13
14
  DEFAULT_BASE_URL = "https://api.hydrafetch.com"
@@ -95,7 +96,10 @@ class _Base:
95
96
  self, urls: str | Iterable[str], options: Mapping[str, Any]
96
97
  ) -> dict[str, Any]:
97
98
  url_list = [urls] if isinstance(urls, str) else list(urls)
98
- return {"urls": url_list, **_drop_none(options)}
99
+ body = _drop_none(options)
100
+ if "schema" in body:
101
+ body["schema"] = to_json_schema(body["schema"])
102
+ return {"urls": url_list, **body}
99
103
 
100
104
  def _job_id(self, payload: Any, *keys: str) -> str:
101
105
  if isinstance(payload, dict):
@@ -107,8 +111,6 @@ class _Base:
107
111
 
108
112
 
109
113
  class Hydrafetch(_Base):
110
- """Synchronous client. See AsyncHydrafetch for the awaitable version."""
111
-
112
114
  def __init__(self, api_key: str | None = None, **kwargs: Any) -> None:
113
115
  super().__init__(api_key, **kwargs)
114
116
  self._http = httpx.Client(
@@ -154,43 +156,33 @@ class Hydrafetch(_Base):
154
156
  raise last or HydrafetchError("Request failed")
155
157
 
156
158
  def scrape(self, url: str, **options: Any) -> dict[str, Any]:
157
- """One page as clean Markdown and structured data. One credit."""
158
159
  return self._request("POST", "/v1/web/scrape", json=self._scrape_body(url, options))
159
160
 
160
161
  def map(self, url: str, **options: Any) -> dict[str, Any]:
161
- """A site's URLs from its sitemap and links, without fetching the pages. One credit."""
162
162
  return self._request("POST", "/v1/web/map", json=self._scrape_body(url, options))
163
163
 
164
164
  def search(self, query: str, **options: Any) -> dict[str, Any]:
165
- """Ranked web results. One credit, plus one per result when scrape_results is set."""
166
165
  return self._request("POST", "/v1/web/search", json={"query": query, **_drop_none(options)})
167
166
 
168
167
  def extract(self, urls: str | Iterable[str], **options: Any) -> Any:
169
- """Typed JSON from one or more URLs. Five credits per URL."""
170
168
  return self._request("POST", "/v1/web/extract", json=self._extract_body(urls, options))
171
169
 
172
170
  def brand(self, domain: str) -> dict[str, Any]:
173
- """A company's brand from its domain. Five credits."""
174
171
  return self._request("GET", "/v1/web/brand", params={"domain": domain})
175
172
 
176
173
  def logo(self, domain: str, **options: Any) -> dict[str, Any]:
177
- """One embeddable logo. One credit, a fifth of a full brand resolve."""
178
174
  return self._request("GET", "/v1/web/brand/logo", params={"domain": domain, **options})
179
175
 
180
176
  def styleguide(self, domain: str) -> dict[str, Any]:
181
- """A site's design system, read from computed styles in a real browser. Ten credits."""
182
177
  return self._request("GET", "/v1/web/styleguide", params={"domain": domain})
183
178
 
184
179
  def screenshot(self, url: str, **options: Any) -> dict[str, Any]:
185
- """A rendered PNG of the page, returned as a public URL. Five credits."""
186
180
  return self._request("POST", "/v1/web/screenshot", json=self._scrape_body(url, options))
187
181
 
188
182
  def images(self, url: str, **options: Any) -> dict[str, Any]:
189
- """A page's images with metadata, without rendering it. One credit."""
190
183
  return self._request("POST", "/v1/web/images", json=self._scrape_body(url, options))
191
184
 
192
185
  def links(self, url: str, **options: Any) -> dict[str, Any]:
193
- """A page's links. One credit."""
194
186
  return self._request("POST", "/v1/web/links", json=self._scrape_body(url, options))
195
187
 
196
188
  def start_crawl(self, url: str, **options: Any) -> str:
@@ -218,7 +210,6 @@ class Hydrafetch(_Base):
218
210
  on_progress: Callable[[dict[str, Any]], None] | None = None,
219
211
  **options: Any,
220
212
  ) -> dict[str, Any]:
221
- """Start a crawl and poll until it reaches a terminal state."""
222
213
  crawl_id = self.start_crawl(url, **options)
223
214
  return self._wait(
224
215
  lambda: self.crawl_status(crawl_id), poll_interval, job_timeout, on_progress
@@ -233,7 +224,6 @@ class Hydrafetch(_Base):
233
224
  on_progress: Callable[[dict[str, Any]], None] | None = None,
234
225
  **options: Any,
235
226
  ) -> dict[str, Any]:
236
- """Start a batch and poll until it reaches a terminal state."""
237
227
  batch_id = self.start_batch(urls, **options)
238
228
  return self._wait(
239
229
  lambda: self.batch_status(batch_id), poll_interval, job_timeout, on_progress
@@ -262,8 +252,6 @@ class Hydrafetch(_Base):
262
252
 
263
253
 
264
254
  class AsyncHydrafetch(_Base):
265
- """Awaitable client with the same surface as Hydrafetch."""
266
-
267
255
  def __init__(self, api_key: str | None = None, **kwargs: Any) -> None:
268
256
  super().__init__(api_key, **kwargs)
269
257
  self._http = httpx.AsyncClient(