hydrafetch 0.3.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.
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/PKG-INFO +46 -2
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/README.md +45 -1
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/pyproject.toml +1 -1
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/src/hydrafetch/__init__.py +1 -1
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/src/hydrafetch/_client.py +83 -0
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/tests/test_client.py +69 -0
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/uv.lock +4 -4
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/.github/workflows/ci.yml +0 -0
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/.github/workflows/publish.yml +0 -0
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/.gitignore +0 -0
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/LICENSE +0 -0
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/src/hydrafetch/_schema.py +0 -0
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/src/hydrafetch/errors.py +0 -0
- {hydrafetch-0.3.0 → hydrafetch-0.5.0}/src/hydrafetch/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hydrafetch
|
|
3
|
-
Version: 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
|
|
@@ -48,7 +48,7 @@ page = hf.scrape("https://example.com/article")
|
|
|
48
48
|
print(page["markdown"])
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
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
|
|
|
@@ -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
|
|
@@ -176,6 +199,24 @@ hf.styleguide("https://stripe.com/pricing", maxAge=0) # one page, read fresh
|
|
|
176
199
|
|
|
177
200
|
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
201
|
|
|
202
|
+
## Rating results
|
|
203
|
+
|
|
204
|
+
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.
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
page = hf.scrape("https://stripe.com/pricing")
|
|
208
|
+
hf.rate_result(
|
|
209
|
+
"bad",
|
|
210
|
+
request_id=hf.last_request_id,
|
|
211
|
+
reasons=["missing_content"],
|
|
212
|
+
note="The enterprise tier is missing",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
hf.rate_result("good", url="https://stripe.com/pricing") # by URL, last 24 hours
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Reasons are `missing_content`, `wrong_content`, `blocked`, `slow`, `formatting` and `other`.
|
|
219
|
+
|
|
179
220
|
## Async
|
|
180
221
|
|
|
181
222
|
`AsyncHydrafetch` mirrors the same surface. Use it when several calls can run concurrently.
|
|
@@ -252,6 +293,8 @@ hf = Hydrafetch(
|
|
|
252
293
|
| `markdown(url, **options)` | `str` | 1 |
|
|
253
294
|
| `map(url, **options)` | links dict | 1 |
|
|
254
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 |
|
|
255
298
|
| `extract(urls, **options)` | dict with `"results"` | 5 per URL |
|
|
256
299
|
| `brand(domain)` | brand dict | 5 |
|
|
257
300
|
| `logo(domain, **options)` | logo dict | 1 |
|
|
@@ -262,6 +305,7 @@ hf = Hydrafetch(
|
|
|
262
305
|
| `batch(urls, **options)` | job dict, polled to completion | 1 per page |
|
|
263
306
|
| `start_crawl`, `start_batch` | job id `str` | 1 per page |
|
|
264
307
|
| `crawl_status(id)`, `batch_status(id)` | job dict | free |
|
|
308
|
+
| `rate_result(rating, request_id=, url=, reasons=, note=)` | rating dict | free |
|
|
265
309
|
|
|
266
310
|
Failed requests are not billed. Pricing does not vary with page difficulty, so there is no render, stealth or proxy option to set.
|
|
267
311
|
|
|
@@ -25,7 +25,7 @@ page = hf.scrape("https://example.com/article")
|
|
|
25
25
|
print(page["markdown"])
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
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
|
|
|
@@ -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
|
|
@@ -153,6 +176,24 @@ hf.styleguide("https://stripe.com/pricing", maxAge=0) # one page, read fresh
|
|
|
153
176
|
|
|
154
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.
|
|
155
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
|
+
|
|
156
197
|
## Async
|
|
157
198
|
|
|
158
199
|
`AsyncHydrafetch` mirrors the same surface. Use it when several calls can run concurrently.
|
|
@@ -229,6 +270,8 @@ hf = Hydrafetch(
|
|
|
229
270
|
| `markdown(url, **options)` | `str` | 1 |
|
|
230
271
|
| `map(url, **options)` | links dict | 1 |
|
|
231
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 |
|
|
232
275
|
| `extract(urls, **options)` | dict with `"results"` | 5 per URL |
|
|
233
276
|
| `brand(domain)` | brand dict | 5 |
|
|
234
277
|
| `logo(domain, **options)` | logo dict | 1 |
|
|
@@ -239,6 +282,7 @@ hf = Hydrafetch(
|
|
|
239
282
|
| `batch(urls, **options)` | job dict, polled to completion | 1 per page |
|
|
240
283
|
| `start_crawl`, `start_batch` | job id `str` | 1 per page |
|
|
241
284
|
| `crawl_status(id)`, `batch_status(id)` | job dict | free |
|
|
285
|
+
| `rate_result(rating, request_id=, url=, reasons=, note=)` | rating dict | free |
|
|
242
286
|
|
|
243
287
|
Failed requests are not billed. Pricing does not vary with page difficulty, so there is no render, stealth or proxy option to set.
|
|
244
288
|
|
|
@@ -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
|
|
@@ -88,6 +89,31 @@ class _Base:
|
|
|
88
89
|
self._base_url = base_url.rstrip("/")
|
|
89
90
|
self._timeout = timeout
|
|
90
91
|
self._max_retries = max_retries
|
|
92
|
+
self.last_request_id: str | None = None
|
|
93
|
+
|
|
94
|
+
def _rate_body(
|
|
95
|
+
self,
|
|
96
|
+
rating: str,
|
|
97
|
+
request_id: str | None,
|
|
98
|
+
url: str | None,
|
|
99
|
+
reasons: Iterable[str] | None,
|
|
100
|
+
note: str | None,
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
if request_id is None and url is None:
|
|
103
|
+
raise HydrafetchError(
|
|
104
|
+
"Pass request_id or url to say which result you are rating.",
|
|
105
|
+
code="VALIDATION_ERROR",
|
|
106
|
+
status=400,
|
|
107
|
+
)
|
|
108
|
+
return _drop_none(
|
|
109
|
+
{
|
|
110
|
+
"requestId": request_id,
|
|
111
|
+
"url": url,
|
|
112
|
+
"rating": rating,
|
|
113
|
+
"reasons": list(reasons) if reasons is not None else None,
|
|
114
|
+
"note": note,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
91
117
|
|
|
92
118
|
def _scrape_body(self, url: str, options: Mapping[str, Any]) -> dict[str, Any]:
|
|
93
119
|
return {"url": url, **_drop_none(options)}
|
|
@@ -105,6 +131,13 @@ class _Base:
|
|
|
105
131
|
body["schema"] = to_json_schema(body["schema"])
|
|
106
132
|
return {"urls": url_list, **body}
|
|
107
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
|
+
|
|
108
141
|
def _job_id(self, payload: Any, *keys: str) -> str:
|
|
109
142
|
if isinstance(payload, dict):
|
|
110
143
|
for key in (*keys, "id"):
|
|
@@ -150,6 +183,7 @@ class Hydrafetch(_Base):
|
|
|
150
183
|
"or use the crawl and batch job endpoints for long work."
|
|
151
184
|
) from exc
|
|
152
185
|
|
|
186
|
+
self.last_request_id = response.headers.get("x-request-id") or self.last_request_id
|
|
153
187
|
try:
|
|
154
188
|
return _unwrap(response)
|
|
155
189
|
except HydrafetchError as err:
|
|
@@ -162,12 +196,36 @@ class Hydrafetch(_Base):
|
|
|
162
196
|
def scrape(self, url: str, **options: Any) -> dict[str, Any]:
|
|
163
197
|
return self._request("POST", "/v1/web/scrape", json=self._scrape_body(url, options))
|
|
164
198
|
|
|
199
|
+
def rate_result(
|
|
200
|
+
self,
|
|
201
|
+
rating: str,
|
|
202
|
+
*,
|
|
203
|
+
request_id: str | None = None,
|
|
204
|
+
url: str | None = None,
|
|
205
|
+
reasons: Iterable[str] | None = None,
|
|
206
|
+
note: str | None = None,
|
|
207
|
+
) -> dict[str, Any]:
|
|
208
|
+
body = self._rate_body(rating, request_id, url, reasons, note)
|
|
209
|
+
return self._request("POST", "/v1/web/feedback", json=body)
|
|
210
|
+
|
|
165
211
|
def map(self, url: str, **options: Any) -> dict[str, Any]:
|
|
166
212
|
return self._request("POST", "/v1/web/map", json=self._scrape_body(url, options))
|
|
167
213
|
|
|
168
214
|
def search(self, query: str, **options: Any) -> dict[str, Any]:
|
|
169
215
|
return self._request("POST", "/v1/web/search", json={"query": query, **_drop_none(options)})
|
|
170
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
|
+
|
|
171
229
|
def extract(self, urls: str | Iterable[str], **options: Any) -> Any:
|
|
172
230
|
return self._request("POST", "/v1/web/extract", json=self._extract_body(urls, options))
|
|
173
231
|
|
|
@@ -295,6 +353,7 @@ class AsyncHydrafetch(_Base):
|
|
|
295
353
|
"or use the crawl and batch job endpoints for long work."
|
|
296
354
|
) from exc
|
|
297
355
|
|
|
356
|
+
self.last_request_id = response.headers.get("x-request-id") or self.last_request_id
|
|
298
357
|
try:
|
|
299
358
|
return _unwrap(response)
|
|
300
359
|
except HydrafetchError as err:
|
|
@@ -307,6 +366,18 @@ class AsyncHydrafetch(_Base):
|
|
|
307
366
|
async def scrape(self, url: str, **options: Any) -> dict[str, Any]:
|
|
308
367
|
return await self._request("POST", "/v1/web/scrape", json=self._scrape_body(url, options))
|
|
309
368
|
|
|
369
|
+
async def rate_result(
|
|
370
|
+
self,
|
|
371
|
+
rating: str,
|
|
372
|
+
*,
|
|
373
|
+
request_id: str | None = None,
|
|
374
|
+
url: str | None = None,
|
|
375
|
+
reasons: Iterable[str] | None = None,
|
|
376
|
+
note: str | None = None,
|
|
377
|
+
) -> dict[str, Any]:
|
|
378
|
+
body = self._rate_body(rating, request_id, url, reasons, note)
|
|
379
|
+
return await self._request("POST", "/v1/web/feedback", json=body)
|
|
380
|
+
|
|
310
381
|
async def map(self, url: str, **options: Any) -> dict[str, Any]:
|
|
311
382
|
return await self._request("POST", "/v1/web/map", json=self._scrape_body(url, options))
|
|
312
383
|
|
|
@@ -315,6 +386,18 @@ class AsyncHydrafetch(_Base):
|
|
|
315
386
|
"POST", "/v1/web/search", json={"query": query, **_drop_none(options)}
|
|
316
387
|
)
|
|
317
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
|
+
|
|
318
401
|
async def extract(self, urls: str | Iterable[str], **options: Any) -> Any:
|
|
319
402
|
return await self._request(
|
|
320
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
|
|
@@ -256,3 +257,71 @@ def test_extract_rejects_a_recursive_model_rather_than_sending_a_ref():
|
|
|
256
257
|
with pytest.raises(HydrafetchError) as err:
|
|
257
258
|
client.extract("https://example.com", schema=Node)
|
|
258
259
|
assert err.value.code == "SCHEMA_CONVERSION_FAILED"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_rate_result_posts_the_rating_for_the_last_request() -> None:
|
|
263
|
+
seen: list[httpx.Request] = []
|
|
264
|
+
|
|
265
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
266
|
+
seen.append(request)
|
|
267
|
+
if request.url.path == "/v1/web/feedback":
|
|
268
|
+
return ok({"success": True, "data": {"id": "f1", "requestId": "r1", "rating": "bad"}})
|
|
269
|
+
return httpx.Response(
|
|
270
|
+
200, json={"success": True, "data": {}}, headers={"x-request-id": "r1"}
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
client = make_client(handler)
|
|
274
|
+
client.scrape("https://a.com")
|
|
275
|
+
assert client.last_request_id == "r1"
|
|
276
|
+
|
|
277
|
+
out = client.rate_result(
|
|
278
|
+
"bad", request_id=client.last_request_id, reasons=["missing_content"], note="no prices"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
assert out == {"id": "f1", "requestId": "r1", "rating": "bad"}
|
|
282
|
+
assert json.loads(seen[-1].content) == {
|
|
283
|
+
"requestId": "r1",
|
|
284
|
+
"rating": "bad",
|
|
285
|
+
"reasons": ["missing_content"],
|
|
286
|
+
"note": "no prices",
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_rate_result_can_rate_by_url_and_needs_a_target() -> None:
|
|
291
|
+
rec = Recorder({"success": True, "data": {"id": "f2", "requestId": "r2", "rating": "good"}})
|
|
292
|
+
client = make_client(rec)
|
|
293
|
+
client.rate_result("good", url="https://a.com")
|
|
294
|
+
assert json.loads(rec.last.content) == {"url": "https://a.com", "rating": "good"}
|
|
295
|
+
with pytest.raises(HydrafetchError):
|
|
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.
|
|
78
|
+
version = "2026.10.19"
|
|
79
79
|
source = { registry = "https://pypi.org/simple" }
|
|
80
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
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/
|
|
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.
|
|
146
|
+
version = "0.5.0"
|
|
147
147
|
source = { editable = "." }
|
|
148
148
|
dependencies = [
|
|
149
149
|
{ name = "httpx" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|