hydrafetch 0.4.0__tar.gz → 0.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hydrafetch
3
- Version: 0.4.0
3
+ Version: 0.5.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
@@ -165,6 +165,29 @@ for r in res["results"]:
165
165
  print(((r.get("data") or {}).get("markdown") or "")[:500])
166
166
  ```
167
167
 
168
+ ## Jobs
169
+
170
+ Search open jobs collected from company career sites and hiring platforms, by the words in the job title:
171
+
172
+ ```python
173
+ jobs = hf.search_jobs("data engineer", location="Berlin", postedWithinDays=14, limit=20)
174
+
175
+ for job in jobs:
176
+ print(job["title"], job["company"], job["location"], job["url"])
177
+ ```
178
+
179
+ See what changed in a company's hiring since a date: what is open now, what opened and what closed, by department, seniority and location, with the jobs themselves:
180
+
181
+ ```python
182
+ [acme] = hf.hiring_changes("acme.com", "2026-10-01")
183
+
184
+ print(acme["totals"]) # {"open": 42, "opened": 6, "closed": 3}
185
+ for job in acme["opened"]:
186
+ print(job["title"], job["seniority"], job["firstSeenAt"])
187
+ ```
188
+
189
+ A search costs 1 credit per 10 jobs returned, and an empty one is free. Hiring changes cost 1 credit per company we have jobs for; companies we do not track yet come back empty and are free.
190
+
168
191
  ## Brand data
169
192
 
170
193
  ```python
@@ -270,6 +293,8 @@ hf = Hydrafetch(
270
293
  | `markdown(url, **options)` | `str` | 1 |
271
294
  | `map(url, **options)` | links dict | 1 |
272
295
  | `search(query, **options)` | results dict | 1 + 1 per scraped result |
296
+ | `search_jobs(query, **options)` | list of jobs | 1 per 10 jobs |
297
+ | `hiring_changes(domains, since, **options)` | list of companies | 1 per company with jobs |
273
298
  | `extract(urls, **options)` | dict with `"results"` | 5 per URL |
274
299
  | `brand(domain)` | brand dict | 5 |
275
300
  | `logo(domain, **options)` | logo dict | 1 |
@@ -142,6 +142,29 @@ for r in res["results"]:
142
142
  print(((r.get("data") or {}).get("markdown") or "")[:500])
143
143
  ```
144
144
 
145
+ ## Jobs
146
+
147
+ Search open jobs collected from company career sites and hiring platforms, by the words in the job title:
148
+
149
+ ```python
150
+ jobs = hf.search_jobs("data engineer", location="Berlin", postedWithinDays=14, limit=20)
151
+
152
+ for job in jobs:
153
+ print(job["title"], job["company"], job["location"], job["url"])
154
+ ```
155
+
156
+ See what changed in a company's hiring since a date: what is open now, what opened and what closed, by department, seniority and location, with the jobs themselves:
157
+
158
+ ```python
159
+ [acme] = hf.hiring_changes("acme.com", "2026-10-01")
160
+
161
+ print(acme["totals"]) # {"open": 42, "opened": 6, "closed": 3}
162
+ for job in acme["opened"]:
163
+ print(job["title"], job["seniority"], job["firstSeenAt"])
164
+ ```
165
+
166
+ A search costs 1 credit per 10 jobs returned, and an empty one is free. Hiring changes cost 1 credit per company we have jobs for; companies we do not track yet come back empty and are free.
167
+
145
168
  ## Brand data
146
169
 
147
170
  ```python
@@ -247,6 +270,8 @@ hf = Hydrafetch(
247
270
  | `markdown(url, **options)` | `str` | 1 |
248
271
  | `map(url, **options)` | links dict | 1 |
249
272
  | `search(query, **options)` | results dict | 1 + 1 per scraped result |
273
+ | `search_jobs(query, **options)` | list of jobs | 1 per 10 jobs |
274
+ | `hiring_changes(domains, since, **options)` | list of companies | 1 per company with jobs |
250
275
  | `extract(urls, **options)` | dict with `"results"` | 5 per URL |
251
276
  | `brand(domain)` | brand dict | 5 |
252
277
  | `logo(domain, **options)` | logo dict | 1 |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hydrafetch"
3
- version = "0.4.0"
3
+ version = "0.5.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.4.0"
7
+ __version__ = "0.5.0"
@@ -4,6 +4,7 @@ import os
4
4
  import random
5
5
  import time
6
6
  from collections.abc import Iterable, Mapping
7
+ from datetime import datetime
7
8
  from typing import Any, Callable, Literal, TypeVar
8
9
 
9
10
  import httpx
@@ -130,6 +131,13 @@ class _Base:
130
131
  body["schema"] = to_json_schema(body["schema"])
131
132
  return {"urls": url_list, **body}
132
133
 
134
+ def _changes_body(
135
+ self, domains: str | Iterable[str], since: str | datetime, options: Mapping[str, Any]
136
+ ) -> dict[str, Any]:
137
+ domain_list = [domains] if isinstance(domains, str) else list(domains)
138
+ when = since.isoformat() if isinstance(since, datetime) else since
139
+ return {"domains": domain_list, "since": when, **_drop_none(options)}
140
+
133
141
  def _job_id(self, payload: Any, *keys: str) -> str:
134
142
  if isinstance(payload, dict):
135
143
  for key in (*keys, "id"):
@@ -206,6 +214,18 @@ class Hydrafetch(_Base):
206
214
  def search(self, query: str, **options: Any) -> dict[str, Any]:
207
215
  return self._request("POST", "/v1/web/search", json={"query": query, **_drop_none(options)})
208
216
 
217
+ def search_jobs(self, query: str, **options: Any) -> list[dict[str, Any]]:
218
+ return self._request(
219
+ "POST", "/v1/web/index/jobs/search", json={"query": query, **_drop_none(options)}
220
+ )
221
+
222
+ def hiring_changes(
223
+ self, domains: str | Iterable[str], since: str | datetime, **options: Any
224
+ ) -> list[dict[str, Any]]:
225
+ return self._request(
226
+ "POST", "/v1/web/index/jobs/changes", json=self._changes_body(domains, since, options)
227
+ )
228
+
209
229
  def extract(self, urls: str | Iterable[str], **options: Any) -> Any:
210
230
  return self._request("POST", "/v1/web/extract", json=self._extract_body(urls, options))
211
231
 
@@ -366,6 +386,18 @@ class AsyncHydrafetch(_Base):
366
386
  "POST", "/v1/web/search", json={"query": query, **_drop_none(options)}
367
387
  )
368
388
 
389
+ async def search_jobs(self, query: str, **options: Any) -> list[dict[str, Any]]:
390
+ return await self._request(
391
+ "POST", "/v1/web/index/jobs/search", json={"query": query, **_drop_none(options)}
392
+ )
393
+
394
+ async def hiring_changes(
395
+ self, domains: str | Iterable[str], since: str | datetime, **options: Any
396
+ ) -> list[dict[str, Any]]:
397
+ return await self._request(
398
+ "POST", "/v1/web/index/jobs/changes", json=self._changes_body(domains, since, options)
399
+ )
400
+
369
401
  async def extract(self, urls: str | Iterable[str], **options: Any) -> Any:
370
402
  return await self._request(
371
403
  "POST", "/v1/web/extract", json=self._extract_body(urls, options)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ from datetime import datetime, timezone
4
5
  from typing import Any, Optional
5
6
 
6
7
  import httpx
@@ -293,3 +294,34 @@ def test_rate_result_can_rate_by_url_and_needs_a_target() -> None:
293
294
  assert json.loads(rec.last.content) == {"url": "https://a.com", "rating": "good"}
294
295
  with pytest.raises(HydrafetchError):
295
296
  client.rate_result("good")
297
+
298
+
299
+ def test_search_jobs_posts_the_query_and_returns_the_list() -> None:
300
+ job = {"title": "Data Engineer", "domain": "acme.com", "board": "greenhouse"}
301
+ rec = Recorder({"success": True, "data": [job]})
302
+ client = make_client(rec)
303
+ jobs = client.search_jobs("data engineer", location="Berlin", limit=20, remoteOnly=None)
304
+ assert jobs == [job]
305
+ assert rec.last.url.path == "/v1/web/index/jobs/search"
306
+ assert json.loads(rec.last.content) == {
307
+ "query": "data engineer",
308
+ "location": "Berlin",
309
+ "limit": 20,
310
+ }
311
+
312
+
313
+ def test_hiring_changes_takes_one_domain_or_many_and_a_datetime() -> None:
314
+ rec = Recorder({"success": True, "data": []})
315
+ client = make_client(rec)
316
+ client.hiring_changes("stripe.com", datetime(2026, 10, 1, tzinfo=timezone.utc))
317
+ assert rec.last.url.path == "/v1/web/index/jobs/changes"
318
+ assert json.loads(rec.last.content) == {
319
+ "domains": ["stripe.com"],
320
+ "since": "2026-10-01T00:00:00+00:00",
321
+ }
322
+ client.hiring_changes(["a.com", "b.com"], "2026-10-01", includeDescriptions=True)
323
+ assert json.loads(rec.last.content) == {
324
+ "domains": ["a.com", "b.com"],
325
+ "since": "2026-10-01",
326
+ "includeDescriptions": True,
327
+ }
@@ -75,11 +75,11 @@ wheels = [
75
75
 
76
76
  [[package]]
77
77
  name = "certifi"
78
- version = "2026.7.22"
78
+ version = "2026.10.19"
79
79
  source = { registry = "https://pypi.org/simple" }
80
- sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
80
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" }
81
81
  wheels = [
82
- { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
82
+ { url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" },
83
83
  ]
84
84
 
85
85
  [[package]]
@@ -143,7 +143,7 @@ wheels = [
143
143
 
144
144
  [[package]]
145
145
  name = "hydrafetch"
146
- version = "0.4.0"
146
+ version = "0.5.0"
147
147
  source = { editable = "." }
148
148
  dependencies = [
149
149
  { name = "httpx" },
File without changes
File without changes