@datafuel/sdk 0.3.0 → 0.4.0
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.
- package/README.md +33 -7
- package/dist/index.cjs +64 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -7
- package/dist/index.d.ts +62 -7
- package/dist/index.js +63 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +44 -2
- package/src/core.ts +29 -20
- package/src/errors.ts +12 -1
- package/src/index.ts +4 -0
- package/src/models.ts +33 -3
package/README.md
CHANGED
|
@@ -26,6 +26,8 @@ const markdown = await df.markdown("https://example.com");
|
|
|
26
26
|
| A Google search | `search` | yes |
|
|
27
27
|
| Many prompts or searches | `runAskJob` / `runSearchJob` | yes |
|
|
28
28
|
| Earlier jobs, tasks, usage | `listJobs` / `listTasks` / `analytics` / `transactions` | yes |
|
|
29
|
+
| What a request may contain | `jsInstructions` / `aiProviders` / `proxyLocations` | yes |
|
|
30
|
+
| The wall in front of a URL | `checkProtection` | yes |
|
|
29
31
|
|
|
30
32
|
Start with plain `scrape`. Turn on `jsRendering` only when the page comes back empty: it is slower and costs five times the credits on a Basic proxy. `map` a section before you `crawl` it, it costs one credit and tells you how big it is.
|
|
31
33
|
|
|
@@ -63,7 +65,22 @@ try {
|
|
|
63
65
|
|
|
64
66
|
`res.text` returns html or markdown, `res.data` structured output, `res.image` screenshot bytes.
|
|
65
67
|
|
|
66
|
-
Page options, shared by `scrape`, jobs and crawls: `format` (`html`, `markdown`, `json`, `png`, `jpeg`), `jsRendering`, `waitFor`, `waitForTimeoutMs`, `jsInstructions` (
|
|
68
|
+
Page options, shared by `scrape`, jobs and crawls: `format` (`html`, `markdown`, `json`, `png`, `jpeg`), `jsRendering`, `waitFor`, `waitForTimeoutMs`, `jsInstructions`, `blockResource` (one resource type or an array, e.g. `["Image", "Font"]`), `mainContentOnly`, `includeImages`, `extract`, `extractRegex`, `template`, `method`, `body`, `contentType`, `headers`, `headerOrder`, `cookies`, `userAgent`, `userAgentType`, `ai`.
|
|
69
|
+
|
|
70
|
+
`jsInstructions` is an array of single-action objects. They run in the order you list them and an action can repeat:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
await df.scrape(url, {
|
|
74
|
+
jsRendering: true,
|
|
75
|
+
jsInstructions: [
|
|
76
|
+
{ fill: ["input[name=q]", "laptops"] },
|
|
77
|
+
{ click: "button[type=submit]" },
|
|
78
|
+
{ wait_ms: 1000 },
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The older form, one object keyed by action (`{ click: "#more" }`), still works, but its order is not guaranteed and an action cannot repeat. `df.jsInstructions()` lists the actions.
|
|
67
84
|
|
|
68
85
|
`proxy` takes `type`, `country`, `city`, `state`, `asn`, and a sticky `sessionId` with `ttl` (seconds) on `scrape`, `map`, URL jobs and crawls. `df.proxyLocations()` and `df.proxyAsns(country)` list what a proxy type can exit from.
|
|
69
86
|
|
|
@@ -76,15 +93,14 @@ const res = await df.scrape(url, {
|
|
|
76
93
|
ai: {
|
|
77
94
|
prompt: "extract the product name and its price",
|
|
78
95
|
format: { name: "string", price: "number" },
|
|
79
|
-
provider: "openai", //
|
|
80
|
-
model: "gpt-4o-mini",
|
|
96
|
+
provider: "openai", // required; df.aiProviders() lists the providers and their models
|
|
81
97
|
apiKey: process.env.OPENAI_API_KEY,
|
|
82
98
|
},
|
|
83
99
|
});
|
|
84
100
|
res.data; // { name: "...", price: ... }
|
|
85
101
|
```
|
|
86
102
|
|
|
87
|
-
Works on `scrape` and on URL jobs. Crawls reject it; the SDK says so before sending. `provider
|
|
103
|
+
Works on `scrape` and on URL jobs. Crawls reject it; the SDK says so before sending. `provider` is required. `model` is optional: leave it out for the provider's default, or pick one of the models `df.aiProviders()` lists; any other value is rejected with `InvalidAttributes`.
|
|
88
104
|
|
|
89
105
|
## Crawl
|
|
90
106
|
|
|
@@ -141,7 +157,7 @@ const serp = await df.search("best crm", { country: "us", language: "en", page:
|
|
|
141
157
|
console.log(serp.data); // parsed results page; format "html" or "markdown" for the raw page
|
|
142
158
|
```
|
|
143
159
|
|
|
144
|
-
`search` also takes `location` (or `uule`, or `lat`/`lon` with `radius`), `googleDomain`, `tbs`, `safe`, `cr`, `lr`, `nfpr
|
|
160
|
+
`search` also takes `location` (or `uule`, or `lat`/`lon` with `radius`), `googleDomain`, `tbs`, `safe`, `cr`, `lr`, `nfpr` and `filter`. Searches leave through DataFuel's own pool: pick the market with `country` and `language`; `proxyCountry` is accepted but not used yet.
|
|
145
161
|
|
|
146
162
|
## Map
|
|
147
163
|
|
|
@@ -172,6 +188,16 @@ const { plan_balance, payg_balance } = await df.balanceSplit();
|
|
|
172
188
|
|
|
173
189
|
`balance` is what you can spend. It is made of plan credits and pay-as-you-go credits. Plan credits are spent first; unused ones roll over when the plan renews and expire if it is not renewed. Pay-as-you-go credits come from one-time credit packs (a `purchase` transaction), are spent after plan credits and never expire. Each transaction's `plan_amount` is the part of `amount` that moved plan credits.
|
|
174
190
|
|
|
191
|
+
## Config and health
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
await df.capabilities(); // which task types and LLM engines are on
|
|
195
|
+
await df.jsInstructions(); // the browser actions jsInstructions accepts
|
|
196
|
+
await df.aiProviders(); // the LLM providers and models ai accepts
|
|
197
|
+
await df.checkProtection(["https://shop.example.com/"]); // anti-bot vendor per URL, nothing scraped
|
|
198
|
+
const health = await df.health({ deep: true }); // health.ok is false while a dependency is down
|
|
199
|
+
```
|
|
200
|
+
|
|
175
201
|
## Errors
|
|
176
202
|
|
|
177
203
|
```ts
|
|
@@ -191,7 +217,7 @@ try {
|
|
|
191
217
|
```
|
|
192
218
|
|
|
193
219
|
- `NoApiKey`: no key was passed and `DATAFUEL_API_KEY` is empty. Thrown before any request.
|
|
194
|
-
- `APIError`: the API refused the request. `.code` holds the API's error code (typed as `ErrorCode`). Subclasses: `Unauthorized`, `Forbidden`, `InsufficientCredits`, `RateLimited`, `NotFound`, `InvalidAttributes`, `IdempotencyKeyReused`, `JobNotCancellable
|
|
220
|
+
- `APIError`: the API refused the request. `.code` holds the API's error code (typed as `ErrorCode`). Subclasses: `Unauthorized`, `Forbidden`, `InsufficientCredits`, `RateLimited`, `NotFound`, `InvalidAttributes`, `IdempotencyKeyReused`, `JobNotCancellable`, `AlreadyExists` (a create collided with an existing task or job; nothing was charged, send it again).
|
|
195
221
|
- `ModuleUnavailable`, `EngineUnavailable`: an operator switched a task type or LLM engine off, e.g. during a provider outage. The reason is in the message, nothing is charged, and the SDK does not retry. `df.capabilities()` lists what is on.
|
|
196
222
|
- `TaskFailed`, and `Blocked` when the target refused: the API accepted the task but the page could not be scraped. The error carries `.result`, so the envelope is still readable. Failed tasks are refunded.
|
|
197
223
|
- `WaitTimeout`: a wait ran out of time. `.id` picks the work back up.
|
|
@@ -224,7 +250,7 @@ Keep `timeoutMs` generous: `scrape` waits until the page is ready, which can tak
|
|
|
224
250
|
|
|
225
251
|
If you pass your own `fetch`, leave redirects off. fetch keeps custom headers across a redirect, so a redirect to another host would carry your `X-API-Key` to it. This SDK sends `redirect: "manual"`.
|
|
226
252
|
|
|
227
|
-
|
|
253
|
+
Guides and the full API reference: https://docs.datafuel.ai
|
|
228
254
|
|
|
229
255
|
## MCP
|
|
230
256
|
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
APIError: () => APIError,
|
|
24
|
+
AlreadyExists: () => AlreadyExists,
|
|
24
25
|
Blocked: () => Blocked,
|
|
25
26
|
Capabilities: () => Capabilities,
|
|
26
27
|
CrawlPage: () => CrawlPage,
|
|
@@ -87,6 +88,8 @@ var IdempotencyKeyReused = class extends APIError {
|
|
|
87
88
|
};
|
|
88
89
|
var JobNotCancellable = class extends APIError {
|
|
89
90
|
};
|
|
91
|
+
var AlreadyExists = class extends APIError {
|
|
92
|
+
};
|
|
90
93
|
var Unavailable = class extends APIError {
|
|
91
94
|
};
|
|
92
95
|
var ModuleUnavailable = class extends Unavailable {
|
|
@@ -124,14 +127,15 @@ var BY_CODE = {
|
|
|
124
127
|
IDEMPOTENCY_KEY_REUSED: IdempotencyKeyReused,
|
|
125
128
|
INVALID_API_KEY: Unauthorized,
|
|
126
129
|
FORBIDDEN: Forbidden,
|
|
127
|
-
JOB_NOT_CANCELLABLE: JobNotCancellable
|
|
130
|
+
JOB_NOT_CANCELLABLE: JobNotCancellable,
|
|
131
|
+
TASK_ALREADY_EXISTS: AlreadyExists,
|
|
132
|
+
JOB_ALREADY_EXISTS: AlreadyExists
|
|
128
133
|
};
|
|
129
134
|
var BY_STATUS = {
|
|
130
135
|
401: Unauthorized,
|
|
131
136
|
402: InsufficientCredits,
|
|
132
137
|
403: Forbidden,
|
|
133
138
|
404: NotFound,
|
|
134
|
-
409: JobNotCancellable,
|
|
135
139
|
422: IdempotencyKeyReused,
|
|
136
140
|
429: RateLimited,
|
|
137
141
|
503: Unavailable
|
|
@@ -152,19 +156,20 @@ function apiError(status, body, retryAfter = 0) {
|
|
|
152
156
|
|
|
153
157
|
// src/core.ts
|
|
154
158
|
var DEFAULT_BASE_URL = "https://scraping-api.datafuel.ai/api/v1";
|
|
155
|
-
var VERSION = "0.
|
|
159
|
+
var VERSION = "0.4.0";
|
|
156
160
|
var DEFAULT_TIMEOUT_MS = 18e4;
|
|
157
161
|
var STILL_PROCESSING_DELAY_MS = 2e3;
|
|
158
162
|
var STILL_PROCESSING = "TASK_STILL_PROCESSING";
|
|
159
163
|
var MAX_IDEMPOTENCY_KEY = 255;
|
|
160
164
|
var Request = class {
|
|
161
|
-
constructor(method, path, params, body, idempotencyKey, auth = true) {
|
|
165
|
+
constructor(method, path, params, body, idempotencyKey, auth = true, degradedOk = false) {
|
|
162
166
|
this.method = method;
|
|
163
167
|
this.path = path;
|
|
164
168
|
this.params = params;
|
|
165
169
|
this.body = body;
|
|
166
170
|
this.idempotencyKey = idempotencyKey;
|
|
167
171
|
this.auth = auth;
|
|
172
|
+
this.degradedOk = degradedOk;
|
|
168
173
|
}
|
|
169
174
|
method;
|
|
170
175
|
path;
|
|
@@ -172,6 +177,7 @@ var Request = class {
|
|
|
172
177
|
body;
|
|
173
178
|
idempotencyKey;
|
|
174
179
|
auth;
|
|
180
|
+
degradedOk;
|
|
175
181
|
/** GETs are safe by nature, writes because they carry an idempotency key. */
|
|
176
182
|
get retryable() {
|
|
177
183
|
return this.method === "GET" || this.idempotencyKey !== void 0;
|
|
@@ -219,15 +225,8 @@ function aiAttributes(ai) {
|
|
|
219
225
|
return out;
|
|
220
226
|
}
|
|
221
227
|
function validateAI(ai) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
model: ai.model !== void 0,
|
|
225
|
-
apiKey: ai.apiKey !== void 0
|
|
226
|
-
};
|
|
227
|
-
const present = Object.values(given).filter(Boolean).length;
|
|
228
|
-
if (present > 0 && present < 3) {
|
|
229
|
-
const missing = Object.entries(given).filter(([, ok]) => !ok).map(([name]) => name).join(", ");
|
|
230
|
-
throw new TypeError(`ai needs provider, model and apiKey together; missing: ${missing}`);
|
|
228
|
+
if (!ai.provider) {
|
|
229
|
+
throw new TypeError("ai needs a provider; aiProviders() lists the ones the API supports");
|
|
231
230
|
}
|
|
232
231
|
}
|
|
233
232
|
function scrapeAttributes(options = {}) {
|
|
@@ -237,7 +236,9 @@ function scrapeAttributes(options = {}) {
|
|
|
237
236
|
if (options.waitFor) attrs.wait_for_selector = options.waitFor;
|
|
238
237
|
if (options.waitForTimeoutMs) attrs.wait_for_selector_timeout_ms = options.waitForTimeoutMs;
|
|
239
238
|
if (options.jsInstructions) attrs.js_instructions = options.jsInstructions;
|
|
240
|
-
if (options.blockResource)
|
|
239
|
+
if (options.blockResource?.length) {
|
|
240
|
+
attrs.block_resource = typeof options.blockResource === "string" ? options.blockResource : [...options.blockResource];
|
|
241
|
+
}
|
|
241
242
|
if (options.mainContentOnly) attrs.main_content_only = true;
|
|
242
243
|
if (options.includeImages !== void 0) attrs.include_images = options.includeImages;
|
|
243
244
|
if (options.extract) attrs.extract_selector = selector(options.extract);
|
|
@@ -419,6 +420,17 @@ function crawlResultsRequest(crawlId, cursor, limit) {
|
|
|
419
420
|
Object.keys(params).length > 0 ? params : void 0
|
|
420
421
|
);
|
|
421
422
|
}
|
|
423
|
+
function healthRequest(deep) {
|
|
424
|
+
return new Request(
|
|
425
|
+
"GET",
|
|
426
|
+
"/healthz",
|
|
427
|
+
deep ? { deep: "1" } : void 0,
|
|
428
|
+
void 0,
|
|
429
|
+
void 0,
|
|
430
|
+
false,
|
|
431
|
+
true
|
|
432
|
+
);
|
|
433
|
+
}
|
|
422
434
|
function day(value) {
|
|
423
435
|
return typeof value === "string" ? value : value.toISOString().slice(0, 10);
|
|
424
436
|
}
|
|
@@ -651,7 +663,7 @@ var DataFuel = class {
|
|
|
651
663
|
const signal = this.signalFor(opts);
|
|
652
664
|
const send = this.fetchImpl;
|
|
653
665
|
const response = await send(url, signal ? { ...init, signal } : init);
|
|
654
|
-
return await parse(response);
|
|
666
|
+
return await parse(response, request.degradedOk);
|
|
655
667
|
} catch (caught) {
|
|
656
668
|
if (isAbort(caught)) {
|
|
657
669
|
throw new TransportError("the request was aborted or timed out", { cause: caught });
|
|
@@ -1013,6 +1025,37 @@ var DataFuel = class {
|
|
|
1013
1025
|
const body = record(await this.send(request, options));
|
|
1014
1026
|
return Array.isArray(body.instructions) ? body.instructions : [];
|
|
1015
1027
|
}
|
|
1028
|
+
/** The LLM providers and models `ai` accepts. Needs no key. */
|
|
1029
|
+
async aiProviders(options = {}) {
|
|
1030
|
+
const request = new Request(
|
|
1031
|
+
"GET",
|
|
1032
|
+
"/config/ai-providers",
|
|
1033
|
+
void 0,
|
|
1034
|
+
void 0,
|
|
1035
|
+
void 0,
|
|
1036
|
+
false
|
|
1037
|
+
);
|
|
1038
|
+
const body = record(await this.send(request, options));
|
|
1039
|
+
return Array.isArray(body.providers) ? body.providers : [];
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Whether the API is up. `deep` also checks the dependencies it needs to
|
|
1043
|
+
* serve scrapes. A degraded report is returned, not thrown: read `ok`.
|
|
1044
|
+
* Needs no key.
|
|
1045
|
+
*/
|
|
1046
|
+
async health(options = {}) {
|
|
1047
|
+
const body = record(await this.send(healthRequest(options.deep ?? false), options));
|
|
1048
|
+
return { ...body, ok: body.status === "ok" };
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Which anti-bot protection sits in front of each URL. Nothing is scraped
|
|
1052
|
+
* and nothing is charged; invalid URLs are skipped.
|
|
1053
|
+
*/
|
|
1054
|
+
async checkProtection(urls, options = {}) {
|
|
1055
|
+
return list(
|
|
1056
|
+
await this.send(new Request("POST", "/filter/check", void 0, [...urls]), options)
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1016
1059
|
/** Countries, regions and cities a proxy type can exit from. */
|
|
1017
1060
|
async proxyLocations(options = {}) {
|
|
1018
1061
|
const params = options.proxyType ? { proxy_type: options.proxyType } : void 0;
|
|
@@ -1084,7 +1127,7 @@ var DataFuel = class {
|
|
|
1084
1127
|
function envApiKey() {
|
|
1085
1128
|
return typeof process !== "undefined" ? process.env?.DATAFUEL_API_KEY : void 0;
|
|
1086
1129
|
}
|
|
1087
|
-
async function parse(response) {
|
|
1130
|
+
async function parse(response, degradedOk = false) {
|
|
1088
1131
|
const text = await response.text();
|
|
1089
1132
|
let body;
|
|
1090
1133
|
if (text.length > 0) {
|
|
@@ -1094,6 +1137,7 @@ async function parse(response) {
|
|
|
1094
1137
|
body = text;
|
|
1095
1138
|
}
|
|
1096
1139
|
}
|
|
1140
|
+
if (degradedOk && response.status === 503 && isHealthReport(body)) return body;
|
|
1097
1141
|
if (!response.ok) {
|
|
1098
1142
|
const header = response.headers.get("Retry-After");
|
|
1099
1143
|
const retryAfter = header !== null && !Number.isNaN(Number(header)) ? Number(header) : 0;
|
|
@@ -1101,6 +1145,9 @@ async function parse(response) {
|
|
|
1101
1145
|
}
|
|
1102
1146
|
return body;
|
|
1103
1147
|
}
|
|
1148
|
+
function isHealthReport(body) {
|
|
1149
|
+
return body !== null && typeof body === "object" && "status" in body;
|
|
1150
|
+
}
|
|
1104
1151
|
function isAbort(error) {
|
|
1105
1152
|
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
1106
1153
|
}
|
|
@@ -1158,6 +1205,7 @@ function intField(body, name) {
|
|
|
1158
1205
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1159
1206
|
0 && (module.exports = {
|
|
1160
1207
|
APIError,
|
|
1208
|
+
AlreadyExists,
|
|
1161
1209
|
Blocked,
|
|
1162
1210
|
Capabilities,
|
|
1163
1211
|
CrawlPage,
|