hydrafetch 0.3.0__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hydrafetch
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Official Python client for the Hydrafetch web data API. Turn any URL into clean Markdown and structured data.
5
5
  Project-URL: Homepage, https://hydrafetch.com
6
6
  Project-URL: Documentation, https://docs.hydrafetch.com
@@ -48,7 +48,7 @@ page = hf.scrape("https://example.com/article")
48
48
  print(page["markdown"])
49
49
  ```
50
50
 
51
- Create a key at [app.hydrafetch.com](https://app.hydrafetch.com). The constructor reads `HYDRAFETCH_API_KEY` when no key is passed.
51
+ Copy your key from [app.hydrafetch.com](https://app.hydrafetch.com). The constructor reads `HYDRAFETCH_API_KEY` when no key is passed.
52
52
 
53
53
  Both clients are context managers, which closes the connection pool deterministically:
54
54
 
@@ -176,6 +176,24 @@ hf.styleguide("https://stripe.com/pricing", maxAge=0) # one page, read fresh
176
176
 
177
177
  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.
178
178
 
179
+ ## Rating results
180
+
181
+ Tell us when a result was wrong, so it gets looked at. It is free, and rating the same request again replaces the earlier rating.
182
+
183
+ ```python
184
+ page = hf.scrape("https://stripe.com/pricing")
185
+ hf.rate_result(
186
+ "bad",
187
+ request_id=hf.last_request_id,
188
+ reasons=["missing_content"],
189
+ note="The enterprise tier is missing",
190
+ )
191
+
192
+ hf.rate_result("good", url="https://stripe.com/pricing") # by URL, last 24 hours
193
+ ```
194
+
195
+ Reasons are `missing_content`, `wrong_content`, `blocked`, `slow`, `formatting` and `other`.
196
+
179
197
  ## Async
180
198
 
181
199
  `AsyncHydrafetch` mirrors the same surface. Use it when several calls can run concurrently.
@@ -262,6 +280,7 @@ hf = Hydrafetch(
262
280
  | `batch(urls, **options)` | job dict, polled to completion | 1 per page |
263
281
  | `start_crawl`, `start_batch` | job id `str` | 1 per page |
264
282
  | `crawl_status(id)`, `batch_status(id)` | job dict | free |
283
+ | `rate_result(rating, request_id=, url=, reasons=, note=)` | rating dict | free |
265
284
 
266
285
  Failed requests are not billed. Pricing does not vary with page difficulty, so there is no render, stealth or proxy option to set.
267
286
 
@@ -25,7 +25,7 @@ page = hf.scrape("https://example.com/article")
25
25
  print(page["markdown"])
26
26
  ```
27
27
 
28
- Create a key at [app.hydrafetch.com](https://app.hydrafetch.com). The constructor reads `HYDRAFETCH_API_KEY` when no key is passed.
28
+ Copy your key from [app.hydrafetch.com](https://app.hydrafetch.com). The constructor reads `HYDRAFETCH_API_KEY` when no key is passed.
29
29
 
30
30
  Both clients are context managers, which closes the connection pool deterministically:
31
31
 
@@ -153,6 +153,24 @@ hf.styleguide("https://stripe.com/pricing", maxAge=0) # one page, read fresh
153
153
 
154
154
  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.
155
155
 
156
+ ## Rating results
157
+
158
+ Tell us when a result was wrong, so it gets looked at. It is free, and rating the same request again replaces the earlier rating.
159
+
160
+ ```python
161
+ page = hf.scrape("https://stripe.com/pricing")
162
+ hf.rate_result(
163
+ "bad",
164
+ request_id=hf.last_request_id,
165
+ reasons=["missing_content"],
166
+ note="The enterprise tier is missing",
167
+ )
168
+
169
+ hf.rate_result("good", url="https://stripe.com/pricing") # by URL, last 24 hours
170
+ ```
171
+
172
+ Reasons are `missing_content`, `wrong_content`, `blocked`, `slow`, `formatting` and `other`.
173
+
156
174
  ## Async
157
175
 
158
176
  `AsyncHydrafetch` mirrors the same surface. Use it when several calls can run concurrently.
@@ -239,6 +257,7 @@ hf = Hydrafetch(
239
257
  | `batch(urls, **options)` | job dict, polled to completion | 1 per page |
240
258
  | `start_crawl`, `start_batch` | job id `str` | 1 per page |
241
259
  | `crawl_status(id)`, `batch_status(id)` | job dict | free |
260
+ | `rate_result(rating, request_id=, url=, reasons=, note=)` | rating dict | free |
242
261
 
243
262
  Failed requests are not billed. Pricing does not vary with page difficulty, so there is no render, stealth or proxy option to set.
244
263
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hydrafetch"
3
- version = "0.3.0"
3
+ version = "0.4.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"
@@ -4,4 +4,4 @@ from ._client import AsyncHydrafetch, Hydrafetch
4
4
  from .errors import HydrafetchError, HydrafetchTimeout
5
5
 
6
6
  __all__ = ["AsyncHydrafetch", "Hydrafetch", "HydrafetchError", "HydrafetchTimeout"]
7
- __version__ = "0.1.0"
7
+ __version__ = "0.4.0"
@@ -88,6 +88,31 @@ class _Base:
88
88
  self._base_url = base_url.rstrip("/")
89
89
  self._timeout = timeout
90
90
  self._max_retries = max_retries
91
+ self.last_request_id: str | None = None
92
+
93
+ def _rate_body(
94
+ self,
95
+ rating: str,
96
+ request_id: str | None,
97
+ url: str | None,
98
+ reasons: Iterable[str] | None,
99
+ note: str | None,
100
+ ) -> dict[str, Any]:
101
+ if request_id is None and url is None:
102
+ raise HydrafetchError(
103
+ "Pass request_id or url to say which result you are rating.",
104
+ code="VALIDATION_ERROR",
105
+ status=400,
106
+ )
107
+ return _drop_none(
108
+ {
109
+ "requestId": request_id,
110
+ "url": url,
111
+ "rating": rating,
112
+ "reasons": list(reasons) if reasons is not None else None,
113
+ "note": note,
114
+ }
115
+ )
91
116
 
92
117
  def _scrape_body(self, url: str, options: Mapping[str, Any]) -> dict[str, Any]:
93
118
  return {"url": url, **_drop_none(options)}
@@ -150,6 +175,7 @@ class Hydrafetch(_Base):
150
175
  "or use the crawl and batch job endpoints for long work."
151
176
  ) from exc
152
177
 
178
+ self.last_request_id = response.headers.get("x-request-id") or self.last_request_id
153
179
  try:
154
180
  return _unwrap(response)
155
181
  except HydrafetchError as err:
@@ -162,6 +188,18 @@ class Hydrafetch(_Base):
162
188
  def scrape(self, url: str, **options: Any) -> dict[str, Any]:
163
189
  return self._request("POST", "/v1/web/scrape", json=self._scrape_body(url, options))
164
190
 
191
+ def rate_result(
192
+ self,
193
+ rating: str,
194
+ *,
195
+ request_id: str | None = None,
196
+ url: str | None = None,
197
+ reasons: Iterable[str] | None = None,
198
+ note: str | None = None,
199
+ ) -> dict[str, Any]:
200
+ body = self._rate_body(rating, request_id, url, reasons, note)
201
+ return self._request("POST", "/v1/web/feedback", json=body)
202
+
165
203
  def map(self, url: str, **options: Any) -> dict[str, Any]:
166
204
  return self._request("POST", "/v1/web/map", json=self._scrape_body(url, options))
167
205
 
@@ -295,6 +333,7 @@ class AsyncHydrafetch(_Base):
295
333
  "or use the crawl and batch job endpoints for long work."
296
334
  ) from exc
297
335
 
336
+ self.last_request_id = response.headers.get("x-request-id") or self.last_request_id
298
337
  try:
299
338
  return _unwrap(response)
300
339
  except HydrafetchError as err:
@@ -307,6 +346,18 @@ class AsyncHydrafetch(_Base):
307
346
  async def scrape(self, url: str, **options: Any) -> dict[str, Any]:
308
347
  return await self._request("POST", "/v1/web/scrape", json=self._scrape_body(url, options))
309
348
 
349
+ async def rate_result(
350
+ self,
351
+ rating: str,
352
+ *,
353
+ request_id: str | None = None,
354
+ url: str | None = None,
355
+ reasons: Iterable[str] | None = None,
356
+ note: str | None = None,
357
+ ) -> dict[str, Any]:
358
+ body = self._rate_body(rating, request_id, url, reasons, note)
359
+ return await self._request("POST", "/v1/web/feedback", json=body)
360
+
310
361
  async def map(self, url: str, **options: Any) -> dict[str, Any]:
311
362
  return await self._request("POST", "/v1/web/map", json=self._scrape_body(url, options))
312
363
 
@@ -256,3 +256,40 @@ def test_extract_rejects_a_recursive_model_rather_than_sending_a_ref():
256
256
  with pytest.raises(HydrafetchError) as err:
257
257
  client.extract("https://example.com", schema=Node)
258
258
  assert err.value.code == "SCHEMA_CONVERSION_FAILED"
259
+
260
+
261
+ def test_rate_result_posts_the_rating_for_the_last_request() -> None:
262
+ seen: list[httpx.Request] = []
263
+
264
+ def handler(request: httpx.Request) -> httpx.Response:
265
+ seen.append(request)
266
+ if request.url.path == "/v1/web/feedback":
267
+ return ok({"success": True, "data": {"id": "f1", "requestId": "r1", "rating": "bad"}})
268
+ return httpx.Response(
269
+ 200, json={"success": True, "data": {}}, headers={"x-request-id": "r1"}
270
+ )
271
+
272
+ client = make_client(handler)
273
+ client.scrape("https://a.com")
274
+ assert client.last_request_id == "r1"
275
+
276
+ out = client.rate_result(
277
+ "bad", request_id=client.last_request_id, reasons=["missing_content"], note="no prices"
278
+ )
279
+
280
+ assert out == {"id": "f1", "requestId": "r1", "rating": "bad"}
281
+ assert json.loads(seen[-1].content) == {
282
+ "requestId": "r1",
283
+ "rating": "bad",
284
+ "reasons": ["missing_content"],
285
+ "note": "no prices",
286
+ }
287
+
288
+
289
+ def test_rate_result_can_rate_by_url_and_needs_a_target() -> None:
290
+ rec = Recorder({"success": True, "data": {"id": "f2", "requestId": "r2", "rating": "good"}})
291
+ client = make_client(rec)
292
+ client.rate_result("good", url="https://a.com")
293
+ assert json.loads(rec.last.content) == {"url": "https://a.com", "rating": "good"}
294
+ with pytest.raises(HydrafetchError):
295
+ client.rate_result("good")
@@ -143,7 +143,7 @@ wheels = [
143
143
 
144
144
  [[package]]
145
145
  name = "hydrafetch"
146
- version = "0.3.0"
146
+ version = "0.4.0"
147
147
  source = { editable = "." }
148
148
  dependencies = [
149
149
  { name = "httpx" },
File without changes
File without changes