@datafuel/sdk 0.2.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 +40 -7
- package/dist/index.cjs +75 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -9
- package/dist/index.d.ts +84 -9
- package/dist/index.js +74 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +57 -4
- package/src/core.ts +29 -20
- package/src/errors.ts +12 -1
- package/src/index.ts +5 -0
- package/src/models.ts +52 -3
package/src/client.ts
CHANGED
|
@@ -4,14 +4,17 @@ import * as core from "./core.js";
|
|
|
4
4
|
import type { AskOptions, CrawlOptions, MapOptions, SearchOptions } from "./core.js";
|
|
5
5
|
import { apiError, DataFuelError, NoApiKey, TransportError, WaitTimeout } from "./errors.js";
|
|
6
6
|
import type {
|
|
7
|
+
AIProvider,
|
|
7
8
|
Analytics,
|
|
8
9
|
AnalyticsOptions,
|
|
10
|
+
BalanceSplit,
|
|
9
11
|
CallOptions,
|
|
10
12
|
CancelResult,
|
|
11
13
|
Capability,
|
|
12
14
|
CrawlResult,
|
|
13
15
|
CrawlResultsPage,
|
|
14
16
|
CrawlStatus,
|
|
17
|
+
Health,
|
|
15
18
|
JobResults,
|
|
16
19
|
JobsPage,
|
|
17
20
|
JobStatus,
|
|
@@ -20,6 +23,7 @@ import type {
|
|
|
20
23
|
ListOptions,
|
|
21
24
|
ListTasksOptions,
|
|
22
25
|
Profile,
|
|
26
|
+
ProtectionCheck,
|
|
23
27
|
ProxyCountry,
|
|
24
28
|
ProxyLocation,
|
|
25
29
|
ScrapeOptions,
|
|
@@ -124,7 +128,7 @@ export class DataFuel {
|
|
|
124
128
|
// none) throws "Illegal invocation".
|
|
125
129
|
const send = this.fetchImpl;
|
|
126
130
|
const response = await send(url, signal ? { ...init, signal } : init);
|
|
127
|
-
return await parse(response);
|
|
131
|
+
return await parse(response, request.degradedOk);
|
|
128
132
|
} catch (caught) {
|
|
129
133
|
if (isAbort(caught)) {
|
|
130
134
|
throw new TransportError("the request was aborted or timed out", { cause: caught });
|
|
@@ -551,6 +555,40 @@ export class DataFuel {
|
|
|
551
555
|
return Array.isArray(body.instructions) ? (body.instructions as JsInstruction[]) : [];
|
|
552
556
|
}
|
|
553
557
|
|
|
558
|
+
/** The LLM providers and models `ai` accepts. Needs no key. */
|
|
559
|
+
async aiProviders(options: CallOptions = {}): Promise<AIProvider[]> {
|
|
560
|
+
const request = new core.Request(
|
|
561
|
+
"GET",
|
|
562
|
+
"/config/ai-providers",
|
|
563
|
+
undefined,
|
|
564
|
+
undefined,
|
|
565
|
+
undefined,
|
|
566
|
+
false,
|
|
567
|
+
);
|
|
568
|
+
const body = record(await this.send(request, options));
|
|
569
|
+
return Array.isArray(body.providers) ? (body.providers as AIProvider[]) : [];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Whether the API is up. `deep` also checks the dependencies it needs to
|
|
574
|
+
* serve scrapes. A degraded report is returned, not thrown: read `ok`.
|
|
575
|
+
* Needs no key.
|
|
576
|
+
*/
|
|
577
|
+
async health(options: CallOptions & { deep?: boolean } = {}): Promise<Health> {
|
|
578
|
+
const body = record(await this.send(core.healthRequest(options.deep ?? false), options));
|
|
579
|
+
return { ...(body as unknown as Health), ok: body.status === "ok" };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Which anti-bot protection sits in front of each URL. Nothing is scraped
|
|
584
|
+
* and nothing is charged; invalid URLs are skipped.
|
|
585
|
+
*/
|
|
586
|
+
async checkProtection(urls: string[], options: CallOptions = {}): Promise<ProtectionCheck[]> {
|
|
587
|
+
return list(
|
|
588
|
+
await this.send(new core.Request("POST", "/filter/check", undefined, [...urls]), options),
|
|
589
|
+
) as ProtectionCheck[];
|
|
590
|
+
}
|
|
591
|
+
|
|
554
592
|
/** Countries, regions and cities a proxy type can exit from. */
|
|
555
593
|
async proxyLocations(
|
|
556
594
|
options: CallOptions & { proxyType?: string } = {},
|
|
@@ -573,14 +611,24 @@ export class DataFuel {
|
|
|
573
611
|
) as ProxyLocation[];
|
|
574
612
|
}
|
|
575
613
|
|
|
576
|
-
/** Remaining credits. */
|
|
614
|
+
/** Remaining credits: plan and pay-as-you-go together. */
|
|
577
615
|
async balance(options: CallOptions = {}): Promise<number> {
|
|
578
616
|
const body = await this.send(new core.Request("GET", "/users/@me/balance"), options);
|
|
579
617
|
return intField(body, "balance");
|
|
580
618
|
}
|
|
581
619
|
|
|
620
|
+
/** Remaining credits by pool: plan credits (spent first) and pay-as-you-go credits. */
|
|
621
|
+
async balanceSplit(options: CallOptions = {}): Promise<BalanceSplit> {
|
|
622
|
+
const body = await this.send(new core.Request("GET", "/users/@me/balance"), options);
|
|
623
|
+
return {
|
|
624
|
+
balance: intField(body, "balance"),
|
|
625
|
+
plan_balance: intField(body, "plan_balance"),
|
|
626
|
+
payg_balance: intField(body, "payg_balance"),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
582
630
|
/**
|
|
583
|
-
* Credit movements, newest first: purchases, usage, refunds, expiry.
|
|
631
|
+
* Credit movements, newest first: plan assignments, credit pack purchases, usage, refunds, expiry.
|
|
584
632
|
* `sums` totals each operation over the whole range, not just this page.
|
|
585
633
|
*/
|
|
586
634
|
async transactions(options: TransactionsOptions & CallOptions = {}): Promise<TransactionsPage> {
|
|
@@ -629,7 +677,7 @@ function envApiKey(): string | undefined {
|
|
|
629
677
|
return typeof process !== "undefined" ? process.env?.DATAFUEL_API_KEY : undefined;
|
|
630
678
|
}
|
|
631
679
|
|
|
632
|
-
async function parse(response: Response): Promise<unknown> {
|
|
680
|
+
async function parse(response: Response, degradedOk = false): Promise<unknown> {
|
|
633
681
|
const text = await response.text();
|
|
634
682
|
let body: unknown;
|
|
635
683
|
if (text.length > 0) {
|
|
@@ -639,6 +687,7 @@ async function parse(response: Response): Promise<unknown> {
|
|
|
639
687
|
body = text;
|
|
640
688
|
}
|
|
641
689
|
}
|
|
690
|
+
if (degradedOk && response.status === 503 && isHealthReport(body)) return body;
|
|
642
691
|
if (!response.ok) {
|
|
643
692
|
const header = response.headers.get("Retry-After");
|
|
644
693
|
const retryAfter = header !== null && !Number.isNaN(Number(header)) ? Number(header) : 0;
|
|
@@ -647,6 +696,10 @@ async function parse(response: Response): Promise<unknown> {
|
|
|
647
696
|
return body;
|
|
648
697
|
}
|
|
649
698
|
|
|
699
|
+
function isHealthReport(body: unknown): boolean {
|
|
700
|
+
return body !== null && typeof body === "object" && "status" in body;
|
|
701
|
+
}
|
|
702
|
+
|
|
650
703
|
function isAbort(error: unknown): boolean {
|
|
651
704
|
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
652
705
|
}
|
package/src/core.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
export const DEFAULT_BASE_URL = "https://scraping-api.datafuel.ai/api/v1";
|
|
22
22
|
|
|
23
23
|
/** Kept in step with package.json by a test; see test/hardening.test.ts. */
|
|
24
|
-
export const VERSION = "0.
|
|
24
|
+
export const VERSION = "0.4.0";
|
|
25
25
|
|
|
26
26
|
/** How long a synchronous call waits for a result before giving up. */
|
|
27
27
|
export const DEFAULT_TIMEOUT_MS = 180_000;
|
|
@@ -37,9 +37,11 @@ export class Request {
|
|
|
37
37
|
readonly method: string,
|
|
38
38
|
readonly path: string,
|
|
39
39
|
readonly params?: Record<string, string>,
|
|
40
|
-
readonly body?: Record<string, unknown
|
|
40
|
+
readonly body?: Record<string, unknown> | unknown[],
|
|
41
41
|
readonly idempotencyKey?: string,
|
|
42
42
|
readonly auth: boolean = true,
|
|
43
|
+
/** A 503 that carries a health report is an answer, not an error. */
|
|
44
|
+
readonly degradedOk: boolean = false,
|
|
43
45
|
) {}
|
|
44
46
|
|
|
45
47
|
/** GETs are safe by nature, writes because they carry an idempotency key. */
|
|
@@ -109,23 +111,10 @@ function aiAttributes(ai: AI): Record<string, unknown> {
|
|
|
109
111
|
return out;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
/**
|
|
113
|
-
* Catch a half-filled AI key before it costs a request. The API does not check
|
|
114
|
-
* these, so a missing one would be spent on a scrape whose AI step then fails.
|
|
115
|
-
*/
|
|
114
|
+
/** The API answers 400 INVALID_ATTRIBUTES without a provider; say so before sending. */
|
|
116
115
|
export function validateAI(ai: AI): void {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
model: ai.model !== undefined,
|
|
120
|
-
apiKey: ai.apiKey !== undefined,
|
|
121
|
-
};
|
|
122
|
-
const present = Object.values(given).filter(Boolean).length;
|
|
123
|
-
if (present > 0 && present < 3) {
|
|
124
|
-
const missing = Object.entries(given)
|
|
125
|
-
.filter(([, ok]) => !ok)
|
|
126
|
-
.map(([name]) => name)
|
|
127
|
-
.join(", ");
|
|
128
|
-
throw new TypeError(`ai needs provider, model and apiKey together; missing: ${missing}`);
|
|
116
|
+
if (!ai.provider) {
|
|
117
|
+
throw new TypeError("ai needs a provider; aiProviders() lists the ones the API supports");
|
|
129
118
|
}
|
|
130
119
|
}
|
|
131
120
|
|
|
@@ -141,7 +130,12 @@ export function scrapeAttributes(options: ScrapeOptions = {}): Record<string, un
|
|
|
141
130
|
if (options.waitFor) attrs.wait_for_selector = options.waitFor;
|
|
142
131
|
if (options.waitForTimeoutMs) attrs.wait_for_selector_timeout_ms = options.waitForTimeoutMs;
|
|
143
132
|
if (options.jsInstructions) attrs.js_instructions = options.jsInstructions;
|
|
144
|
-
if (options.blockResource)
|
|
133
|
+
if (options.blockResource?.length) {
|
|
134
|
+
attrs.block_resource =
|
|
135
|
+
typeof options.blockResource === "string"
|
|
136
|
+
? options.blockResource
|
|
137
|
+
: [...options.blockResource];
|
|
138
|
+
}
|
|
145
139
|
if (options.mainContentOnly) attrs.main_content_only = true;
|
|
146
140
|
if (options.includeImages !== undefined) attrs.include_images = options.includeImages;
|
|
147
141
|
if (options.extract) attrs.extract_selector = selector(options.extract);
|
|
@@ -386,7 +380,10 @@ export interface SearchOptions {
|
|
|
386
380
|
ludocid?: string;
|
|
387
381
|
lsig?: string;
|
|
388
382
|
ibp?: string;
|
|
389
|
-
/**
|
|
383
|
+
/**
|
|
384
|
+
* Accepted but not used yet: searches leave through DataFuel's own pool.
|
|
385
|
+
* Target a market with `country` and `language`.
|
|
386
|
+
*/
|
|
390
387
|
proxyCountry?: string;
|
|
391
388
|
/** Default json. */
|
|
392
389
|
format?: "json" | "html" | "markdown";
|
|
@@ -465,6 +462,18 @@ export function crawlResultsRequest(crawlId: string, cursor?: string, limit?: nu
|
|
|
465
462
|
);
|
|
466
463
|
}
|
|
467
464
|
|
|
465
|
+
export function healthRequest(deep: boolean): Request {
|
|
466
|
+
return new Request(
|
|
467
|
+
"GET",
|
|
468
|
+
"/healthz",
|
|
469
|
+
deep ? { deep: "1" } : undefined,
|
|
470
|
+
undefined,
|
|
471
|
+
undefined,
|
|
472
|
+
false,
|
|
473
|
+
true,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
468
477
|
/** A Date as its UTC day, which is what the API's date filters take. */
|
|
469
478
|
export function day(value: string | Date): string {
|
|
470
479
|
return typeof value === "string" ? value : value.toISOString().slice(0, 10);
|
package/src/errors.ts
CHANGED
|
@@ -45,6 +45,10 @@ export type ErrorCode =
|
|
|
45
45
|
| "JOB_NOT_FOUND"
|
|
46
46
|
| "CRAWL_NOT_FOUND"
|
|
47
47
|
| "JOB_NOT_CANCELLABLE"
|
|
48
|
+
| "TASK_ALREADY_EXISTS"
|
|
49
|
+
| "JOB_ALREADY_EXISTS"
|
|
50
|
+
| "URL_LENGTH_CANNOT_BE_ZERO"
|
|
51
|
+
| "ANALYTICS_FETCH_FAILED"
|
|
48
52
|
| "INVALID_CRAWL_PATTERN"
|
|
49
53
|
| "CRAWL_UNSUPPORTED_OPTION"
|
|
50
54
|
| "INVALID_CURSOR"
|
|
@@ -92,6 +96,12 @@ export class InvalidAttributes extends APIError {}
|
|
|
92
96
|
export class IdempotencyKeyReused extends APIError {}
|
|
93
97
|
/** 409 JOB_NOT_CANCELLABLE: the job or crawl already finished. */
|
|
94
98
|
export class JobNotCancellable extends APIError {}
|
|
99
|
+
/**
|
|
100
|
+
* 409 TASK_ALREADY_EXISTS / JOB_ALREADY_EXISTS: the create collided with an
|
|
101
|
+
* existing task or job and is not an idempotent replay. Nothing was charged;
|
|
102
|
+
* send the request again.
|
|
103
|
+
*/
|
|
104
|
+
export class AlreadyExists extends APIError {}
|
|
95
105
|
/** 503: an operator switched something off, or a dependency is down. The message carries the reason. */
|
|
96
106
|
export class Unavailable extends APIError {}
|
|
97
107
|
/** 503 MODULE_UNAVAILABLE: this task type is switched off. Nothing was charged. */
|
|
@@ -152,6 +162,8 @@ const BY_CODE: Record<string, new (s: number, c: string, m: string, r?: number)
|
|
|
152
162
|
INVALID_API_KEY: Unauthorized,
|
|
153
163
|
FORBIDDEN: Forbidden,
|
|
154
164
|
JOB_NOT_CANCELLABLE: JobNotCancellable,
|
|
165
|
+
TASK_ALREADY_EXISTS: AlreadyExists,
|
|
166
|
+
JOB_ALREADY_EXISTS: AlreadyExists,
|
|
155
167
|
};
|
|
156
168
|
|
|
157
169
|
const BY_STATUS: Record<number, new (s: number, c: string, m: string, r?: number) => APIError> = {
|
|
@@ -159,7 +171,6 @@ const BY_STATUS: Record<number, new (s: number, c: string, m: string, r?: number
|
|
|
159
171
|
402: InsufficientCredits,
|
|
160
172
|
403: Forbidden,
|
|
161
173
|
404: NotFound,
|
|
162
|
-
409: JobNotCancellable,
|
|
163
174
|
422: IdempotencyKeyReused,
|
|
164
175
|
429: RateLimited,
|
|
165
176
|
503: Unavailable,
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export type { ClientOptions } from "./client.js";
|
|
|
32
32
|
export { DEFAULT_BASE_URL, VERSION } from "./core.js";
|
|
33
33
|
export type { AskOptions, CrawlOptions, MapOptions, SearchOptions } from "./core.js";
|
|
34
34
|
export {
|
|
35
|
+
AlreadyExists,
|
|
35
36
|
APIError,
|
|
36
37
|
Blocked,
|
|
37
38
|
DataFuelError,
|
|
@@ -55,9 +56,11 @@ export type { ErrorCode } from "./errors.js";
|
|
|
55
56
|
export { Capabilities, CrawlPage, isDone, Result } from "./models.js";
|
|
56
57
|
export type {
|
|
57
58
|
AI,
|
|
59
|
+
AIProvider,
|
|
58
60
|
Analytics,
|
|
59
61
|
AnalyticsCounts,
|
|
60
62
|
AnalyticsOptions,
|
|
63
|
+
BalanceSplit,
|
|
61
64
|
CallOptions,
|
|
62
65
|
CancelResult,
|
|
63
66
|
Capability,
|
|
@@ -66,6 +69,7 @@ export type {
|
|
|
66
69
|
CrawlStatus,
|
|
67
70
|
Engine,
|
|
68
71
|
Format,
|
|
72
|
+
Health,
|
|
69
73
|
JobResults,
|
|
70
74
|
JobsPage,
|
|
71
75
|
JobStatus,
|
|
@@ -77,6 +81,7 @@ export type {
|
|
|
77
81
|
ListTasksOptions,
|
|
78
82
|
Payload,
|
|
79
83
|
Profile,
|
|
84
|
+
ProtectionCheck,
|
|
80
85
|
Proxy,
|
|
81
86
|
ProxyCountry,
|
|
82
87
|
ProxyLocation,
|
package/src/models.ts
CHANGED
|
@@ -60,7 +60,9 @@ export interface AI {
|
|
|
60
60
|
prompt?: string;
|
|
61
61
|
/** Example JSON object the output must follow. */
|
|
62
62
|
format?: unknown;
|
|
63
|
-
|
|
63
|
+
/** Required. One of the providers `df.aiProviders()` lists; the API answers 400 INVALID_ATTRIBUTES without it. */
|
|
64
|
+
provider?: string;
|
|
65
|
+
/** One of the models `df.aiProviders()` lists for `provider`. Omit for the provider's default. */
|
|
64
66
|
model?: string;
|
|
65
67
|
apiKey?: string;
|
|
66
68
|
}
|
|
@@ -75,9 +77,15 @@ export interface ScrapeOptions {
|
|
|
75
77
|
jsRendering?: boolean;
|
|
76
78
|
waitFor?: string;
|
|
77
79
|
waitForTimeoutMs?: number;
|
|
78
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Browser actions run after load: an array of single-action objects, run in
|
|
82
|
+
* order, e.g. `[{ click: "#more" }, { wait_ms: 1000 }, { click: "#more" }]`.
|
|
83
|
+
* An object keyed by action is still accepted, but its order is not
|
|
84
|
+
* guaranteed and an action cannot repeat. `df.jsInstructions()` lists them.
|
|
85
|
+
*/
|
|
79
86
|
jsInstructions?: unknown;
|
|
80
|
-
|
|
87
|
+
/** Resource types the browser must not load: one, e.g. `"Image"`, or several. */
|
|
88
|
+
blockResource?: string | string[];
|
|
81
89
|
/** Markdown only: always render just the `<main>` / `<article>` container. */
|
|
82
90
|
mainContentOnly?: boolean;
|
|
83
91
|
/** Markdown only: `false` drops images and saves tokens. */
|
|
@@ -414,6 +422,8 @@ export interface TransactionsOptions {
|
|
|
414
422
|
export interface Transaction {
|
|
415
423
|
id: number;
|
|
416
424
|
amount: number;
|
|
425
|
+
/** The part of `amount` that moved plan credits, same sign; the rest moved pay-as-you-go credits. */
|
|
426
|
+
plan_amount?: number;
|
|
417
427
|
operation: TransactionOperation;
|
|
418
428
|
/** What `reference_id` points to, e.g. `task_id` or `job_id`. */
|
|
419
429
|
reference_type: string;
|
|
@@ -508,13 +518,30 @@ export interface Analytics {
|
|
|
508
518
|
by_status_code: StatusCodeBreakdown[];
|
|
509
519
|
}
|
|
510
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Credits by pool. Plan credits are spent first, roll over when the plan renews and
|
|
523
|
+
* expire if it is not renewed. Pay-as-you-go credits come from credit packs, are spent
|
|
524
|
+
* after plan credits and never expire.
|
|
525
|
+
*/
|
|
526
|
+
export interface BalanceSplit {
|
|
527
|
+
/** Total spendable credits: `plan_balance` plus `payg_balance`. */
|
|
528
|
+
balance: number;
|
|
529
|
+
plan_balance: number;
|
|
530
|
+
payg_balance: number;
|
|
531
|
+
}
|
|
532
|
+
|
|
511
533
|
/** The account behind the API key. */
|
|
512
534
|
export interface Profile {
|
|
513
535
|
email: string;
|
|
514
536
|
username: string;
|
|
515
537
|
current_concurrency: number;
|
|
516
538
|
concurrency_limit: number;
|
|
539
|
+
/** Total spendable credits: `plan_credit_balance` plus `payg_credit_balance`. */
|
|
517
540
|
credit_balance: number;
|
|
541
|
+
/** Spent first; expire if the plan is not renewed. */
|
|
542
|
+
plan_credit_balance?: number;
|
|
543
|
+
/** Spent after plan credits; never expire. */
|
|
544
|
+
payg_credit_balance?: number;
|
|
518
545
|
monthly_credit_limit: number;
|
|
519
546
|
}
|
|
520
547
|
|
|
@@ -538,6 +565,28 @@ export interface JsInstruction {
|
|
|
538
565
|
example: unknown;
|
|
539
566
|
}
|
|
540
567
|
|
|
568
|
+
/** An LLM provider `ai.provider` accepts, with the models `ai.model` accepts for it. */
|
|
569
|
+
export interface AIProvider {
|
|
570
|
+
name: string;
|
|
571
|
+
models: string[];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** The anti-bot protection in front of one host and path, from `checkProtection`. */
|
|
575
|
+
export interface ProtectionCheck {
|
|
576
|
+
host: string;
|
|
577
|
+
path: string;
|
|
578
|
+
/** cloudflare, cloudflare_5sec, akamai, imperva, perimeterx or unprotected. */
|
|
579
|
+
protection_type: string;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** The state of the API. `checks` is filled by a deep check only, keyed by dependency. */
|
|
583
|
+
export interface Health {
|
|
584
|
+
status: "ok" | "degraded" | (string & {});
|
|
585
|
+
checks?: Record<string, { status: "ok" | "fail" | (string & {}); latency_ms: number }>;
|
|
586
|
+
/** Whether the API can serve requests. */
|
|
587
|
+
ok: boolean;
|
|
588
|
+
}
|
|
589
|
+
|
|
541
590
|
/** A named proxy location: a city, or an ASN. */
|
|
542
591
|
export interface ProxyLocation {
|
|
543
592
|
code: string;
|