@pipelex/sdk 0.42.0 → 0.43.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 +17 -0
- package/dist/cli/present.d.ts +3 -2
- package/dist/cli/present.js +5 -4
- package/dist/cli/present.js.map +1 -1
- package/dist/cli/script.d.ts +1 -1
- package/dist/client.d.ts +176 -11
- package/dist/client.js +523 -102
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +2 -2
- package/dist/exact-json.d.ts +64 -0
- package/dist/exact-json.js +127 -0
- package/dist/exact-json.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/product-models.d.ts +136 -2
- package/dist/product-models.js +1 -1
- package/dist/runs.d.ts +3 -1
- package/dist/runs.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +4 -1
- package/docs/cli.md +1 -1
- package/docs/errors.md +4 -3
- package/docs/exact-integers.md +32 -0
- package/docs/method-examples.md +89 -0
- package/docs/run-results.md +2 -0
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -12,9 +12,10 @@ import { encodeBundleResources, hasBundleResources } from "./bundle-resources.js
|
|
|
12
12
|
import { prepareInputs as prepareInputsImpl } from "./prepare-inputs.js";
|
|
13
13
|
import { downloadArtifacts as downloadArtifactsImpl, fetchArtifact as fetchArtifactImpl, resolveArtifacts as resolveArtifactsImpl, } from "./artifacts.js";
|
|
14
14
|
import { PipelexExecuteResult, resultsFromExecute } from "./execute-result.js";
|
|
15
|
+
import { ExactJsonUnsupportedError, parseExactJson, stringifyExactJson } from "./exact-json.js";
|
|
15
16
|
import { MAX_TIMER_DELAY_MS, isTimerDelay } from "./timers.js";
|
|
16
17
|
// A pure RUNAWAY guard on the paged-list iterators (`iterateMethods`,
|
|
17
|
-
// `iterateRuns`), deliberately not a coverage limit.
|
|
18
|
+
// `iterateRuns`, `iterateMethodExamples`), deliberately not a coverage limit.
|
|
18
19
|
//
|
|
19
20
|
// It counts TOTAL pages, not empty ones. An earlier version capped consecutive
|
|
20
21
|
// empty pages, which is the wrong axis: an empty page whose cursor ADVANCED is
|
|
@@ -31,7 +32,8 @@ import { MAX_TIMER_DELAY_MS, isTimerDelay } from "./timers.js";
|
|
|
31
32
|
const MAX_PAGES = 10_000;
|
|
32
33
|
/**
|
|
33
34
|
* Follow a paged list's cursor until the server says there is no more, yielding every row —
|
|
34
|
-
* the one loop behind `iterateMethods` and `
|
|
35
|
+
* the one loop behind `iterateMethods`, `iterateRuns` and `iterateMethodExamples`, so they cannot
|
|
36
|
+
* drift apart.
|
|
35
37
|
*
|
|
36
38
|
* It yields lazily: a caller that breaks early stops the requests. It stops when `nextCursor`
|
|
37
39
|
* is `null`, the normal end, and when the server hands back the cursor it was just sent, which
|
|
@@ -79,6 +81,8 @@ async function* followCursor(route, readPage) {
|
|
|
79
81
|
/** Decoders of an answer's bytes: the strict one tells a body that is not UTF-8. */
|
|
80
82
|
const STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
81
83
|
const LENIENT_UTF8 = new TextDecoder("utf-8");
|
|
84
|
+
/** `JSON.parse`, the `JsonReader` an answer is read with unless its route names another. */
|
|
85
|
+
const readJson = (text) => JSON.parse(text);
|
|
82
86
|
const STARTS_RUN = Object.freeze({ mayStartRun: true });
|
|
83
87
|
/** Hosted default — the client composes every endpoint as `{base}/v1/{endpoint}`. */
|
|
84
88
|
export const DEFAULT_API_BASE_URL = "https://api.pipelex.com";
|
|
@@ -127,21 +131,173 @@ function withWirePython(input) {
|
|
|
127
131
|
return python === undefined ? rest : { ...rest, python: serializeMethodFiles(python) };
|
|
128
132
|
}
|
|
129
133
|
/**
|
|
130
|
-
*
|
|
134
|
+
* A request's body as the JSON text it is sent as, written by `stringifyExactJson` in one pass,
|
|
135
|
+
* so a `bigint` read from an answer carrying a person's data goes back as its exact digits, and
|
|
136
|
+
* every getter and `toJSON` in the body runs once. A body JSON cannot carry, one holding a cycle
|
|
137
|
+
* or whose own `toJSON` throws for instance, is the caller's argument: it is refused with
|
|
138
|
+
* `RequestArgumentError`, the failure as its `cause`, before anything is sent, and `method` and
|
|
139
|
+
* `url` name the request in the message.
|
|
140
|
+
*/
|
|
141
|
+
function requestBodyText(method, url, body) {
|
|
142
|
+
try {
|
|
143
|
+
return stringifyExactJson(body);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
147
|
+
throw new RequestArgumentError(`The body of ${method} ${new URL(url).pathname} cannot be written as JSON, so nothing was ` +
|
|
148
|
+
`sent: ${reason}`, { cause: err });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The query string of a product list route, `?` and its parameters in the order given, or `""`
|
|
153
|
+
* when none is kept, as the Python SDK's `_product_query` builds it. Each entry is kept on
|
|
154
|
+
* presence (`!== undefined`), never truthiness: omission means the caller did not ask, while an
|
|
155
|
+
* explicit empty `q`, cursor or date is bad input the API should refuse, not something to drop
|
|
156
|
+
* into an unfiltered query that returns everything and reads as working. `false` is a filter of
|
|
157
|
+
* its own, sent as `false`, and an array is sent as one repeated parameter per value.
|
|
158
|
+
*/
|
|
159
|
+
function productQuery(params) {
|
|
160
|
+
const search = new URLSearchParams();
|
|
161
|
+
for (const [key, value] of Object.entries(params)) {
|
|
162
|
+
if (value === undefined)
|
|
163
|
+
continue;
|
|
164
|
+
if (typeof value === "object")
|
|
165
|
+
for (const item of value)
|
|
166
|
+
search.append(key, item);
|
|
167
|
+
else
|
|
168
|
+
search.set(key, String(value));
|
|
169
|
+
}
|
|
170
|
+
const text = search.toString();
|
|
171
|
+
return text ? `?${text}` : "";
|
|
172
|
+
}
|
|
173
|
+
/** `items` joined as a sentence names them, the last after `conjunction`: `a`, `a or b`, `a, b or c`. */
|
|
174
|
+
function inWords(items, conjunction) {
|
|
175
|
+
if (items.length < 2)
|
|
176
|
+
return items.join("");
|
|
177
|
+
return `${items.slice(0, -1).join(", ")} ${conjunction} ${items[items.length - 1]}`;
|
|
178
|
+
}
|
|
179
|
+
/** What each id a route puts in its path is, as a refusal of it names it. */
|
|
180
|
+
const METHOD_ID_ARGUMENT = "a method id, the method's non-empty bare catalog id";
|
|
181
|
+
const EXAMPLE_ID_ARGUMENT = "an example id, the example's non-empty `example_id`";
|
|
182
|
+
const RUN_ID_ARGUMENT = "a run id, the run's non-empty `pipeline_run_id`";
|
|
183
|
+
const ORG_ID_ARGUMENT = "an organization id, the organization's non-empty `org_id`";
|
|
184
|
+
const API_KEY_ID_ARGUMENT = "an API key id, the key's non-empty `id`";
|
|
185
|
+
/** What the run id of each example write is, as a refusal of it names it. */
|
|
186
|
+
const CREATE_RUN_ID_ARGUMENT = "a run_id naming a run of this method that used these inputs, a non-empty string, or none";
|
|
187
|
+
const RECORD_RUN_ID_ARGUMENT = "a run_id naming a run of this method that used the example's inputs, a non-empty string";
|
|
188
|
+
/**
|
|
189
|
+
* The refusal of an argument a call cannot take, worded as every such refusal is:
|
|
190
|
+
* `<call>() takes <what>; got <got>.`, with what made it unusable as `cause` when there is one.
|
|
191
|
+
*/
|
|
192
|
+
function argumentRefusal(call, what, got, cause) {
|
|
193
|
+
const message = `${call}() takes ${what}; got ${got}.`;
|
|
194
|
+
return new RequestArgumentError(message, cause === undefined ? undefined : { cause });
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* A caller's id, checked to be a non-empty string: the one check every id a call takes goes
|
|
198
|
+
* through, whether it enters a path (`idPathSegment`) or a body (a run id an example write
|
|
199
|
+
* records). Anything else is refused with `RequestArgumentError` before anything is sent, `call`
|
|
200
|
+
* naming the method in the message and `what` the id it takes.
|
|
201
|
+
*/
|
|
202
|
+
function requireNonEmptyId(value, call, what) {
|
|
203
|
+
if (typeof value === "string" && value !== "")
|
|
204
|
+
return value;
|
|
205
|
+
let got;
|
|
206
|
+
if (typeof value === "string")
|
|
207
|
+
got = "an empty string";
|
|
208
|
+
else
|
|
209
|
+
got = value === null ? "null" : typeof value;
|
|
210
|
+
throw argumentRefusal(call, what, got);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* A caller's value as the refusal of a number argument names it: a number by its value, anything
|
|
214
|
+
* else by its type, as `requireNonEmptyId` names a value, and `null` as itself. Never by `String`,
|
|
215
|
+
* which throws for an object with no prototype and would replace the refusal with a `TypeError`.
|
|
216
|
+
*/
|
|
217
|
+
function describeNumberArgument(value) {
|
|
218
|
+
if (typeof value === "number")
|
|
219
|
+
return String(value);
|
|
220
|
+
return value === null ? "null" : typeof value;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* A caller's id as one segment of a route's path, percent-encoded: the one place an id enters a
|
|
224
|
+
* path. `encodeURIComponent` escapes every character a path gives meaning to but the dot, so an id
|
|
225
|
+
* of `.` or `..` would reach the URL as a dot segment, which the URL resolves before the request
|
|
226
|
+
* leaves: `methods/mt_x/examples/..` is sent as `methods/mt_x/`, and deleting that "example"
|
|
227
|
+
* would erase the method. An empty id addresses the route above it the same way, `runs/` for a
|
|
228
|
+
* run. So a value that is not a non-empty string (`requireNonEmptyId`) and a dot segment are each
|
|
229
|
+
* refused before anything is sent, and so is a string holding a lone surrogate, which has no
|
|
230
|
+
* UTF-8 spelling and so no percent-encoding: `encodeURIComponent` throws a `URIError` for it.
|
|
231
|
+
* `call` names the method in the message and `what` the id it takes. An id spelling a dot in
|
|
232
|
+
* percent-encoding (`%2e`) is no dot segment once its `%` is escaped.
|
|
233
|
+
*/
|
|
234
|
+
function idPathSegment(value, call, what) {
|
|
235
|
+
const id = requireNonEmptyId(value, call, what);
|
|
236
|
+
if (id === "." || id === "..") {
|
|
237
|
+
throw argumentRefusal(call, what, `"${id}", a dot segment, which the URL would resolve into another route's path`);
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
return encodeURIComponent(id);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
const got = "a string holding a lone surrogate, which a URL cannot carry";
|
|
244
|
+
throw argumentRefusal(call, what, got, err);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The path of a method route, `methods/{id}`, for a bare catalog id, its segment refused as
|
|
249
|
+
* `idPathSegment` refuses one; `call` names the method in the message. The method routes address
|
|
131
250
|
* the method itself, never one of its versions, and the platform does not parse a suffix there:
|
|
132
251
|
* `mt_x@3` would be looked up as an id of its own and answer `404 not_found`, which reads as a
|
|
133
252
|
* method that does not exist. So a suffixed id is refused before anything is sent, saying how to
|
|
134
253
|
* read what it names. Stripping it instead would answer the draft for a caller that named a
|
|
135
254
|
* version.
|
|
136
255
|
*/
|
|
137
|
-
function methodPath(methodId) {
|
|
138
|
-
|
|
256
|
+
function methodPath(methodId, call) {
|
|
257
|
+
const segment = idPathSegment(methodId, call, METHOD_ID_ARGUMENT);
|
|
258
|
+
if (methodId.includes("@")) {
|
|
139
259
|
throw new RequestArgumentError(`"${methodId}" carries a version suffix, and the method routes take a bare catalog id: ` +
|
|
140
260
|
"they address the method itself, never one of its versions. Strip the suffix with " +
|
|
141
261
|
"parseMethodSelector, and read a published version with getMethodVersion.");
|
|
142
262
|
}
|
|
143
|
-
return `methods/${
|
|
263
|
+
return `methods/${segment}`;
|
|
144
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* The path of one saved example, `methods/{id}/examples/{example_id}`, for a bare catalog id and
|
|
267
|
+
* an example id, each refused as `methodPath` and `idPathSegment` refuse one: an empty example id
|
|
268
|
+
* would address the examples list rather than an example, and `..` the method itself. `call`
|
|
269
|
+
* names the method in the message.
|
|
270
|
+
*/
|
|
271
|
+
function methodExamplePath(methodId, exampleId, call) {
|
|
272
|
+
const path = methodPath(methodId, call);
|
|
273
|
+
return `${path}/examples/${idPathSegment(exampleId, call, EXAMPLE_ID_ARGUMENT)}`;
|
|
274
|
+
}
|
|
275
|
+
const isString = (value) => typeof value === "string";
|
|
276
|
+
/**
|
|
277
|
+
* Each field of a `MethodExample`, what it must be, and the check it must pass: every field the
|
|
278
|
+
* Python SDK's model requires, held to the same strict types it holds them to, so both SDKs refuse
|
|
279
|
+
* the same answers, the ones a caller acts on (`example_id`, `inputs`, `input_revision`,
|
|
280
|
+
* `latest_run_id`) among them. `latest_run_id` must be present, `null` saying there is no latest
|
|
281
|
+
* run, and `input_revision` must be a safe integer, so a string, a boolean, a fraction and an
|
|
282
|
+
* integer past the safe range, which `parseExactJson` reads as a `bigint`, are each refused by
|
|
283
|
+
* both. The one spelling they read apart is a revision of `2.0`: JavaScript reads it as the number
|
|
284
|
+
* `2` and cannot tell it from `2`, while the Python SDK refuses the float.
|
|
285
|
+
*
|
|
286
|
+
* Keyed by the fields of `MethodExample` and checked against them by `satisfies`, so a field
|
|
287
|
+
* added to the interface without its row here, or a row naming no field of it, fails to compile.
|
|
288
|
+
* The rows are checked in the order written.
|
|
289
|
+
*/
|
|
290
|
+
const METHOD_EXAMPLE_FIELDS = {
|
|
291
|
+
example_id: ["a string", isString],
|
|
292
|
+
method_id: ["a string", isString],
|
|
293
|
+
name: ["a string", isString],
|
|
294
|
+
inputs: ["an object", isPlainObject],
|
|
295
|
+
input_revision: ["an integer", (value) => Number.isSafeInteger(value)],
|
|
296
|
+
latest_run_id: ["a string or null", (value) => value === null || isString(value)],
|
|
297
|
+
created_by_user_id: ["a string", isString],
|
|
298
|
+
created_at: ["a string", isString],
|
|
299
|
+
updated_at: ["a string", isString],
|
|
300
|
+
};
|
|
145
301
|
export class PipelexApiClient {
|
|
146
302
|
apiKey;
|
|
147
303
|
baseUrl;
|
|
@@ -216,6 +372,9 @@ export class PipelexApiClient {
|
|
|
216
372
|
*/
|
|
217
373
|
async requestRaw(method, url, options = {}) {
|
|
218
374
|
const hasBody = options.body !== undefined;
|
|
375
|
+
// Written before the request is made, so a body JSON cannot carry is refused as the caller's
|
|
376
|
+
// argument rather than caught below and reported as a network failure.
|
|
377
|
+
const bodyText = hasBody ? requestBodyText(method, url, options.body) : undefined;
|
|
219
378
|
const headers = this.requestHeaders(hasBody, options.idempotencyKey);
|
|
220
379
|
const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
221
380
|
// A longer delay overflows the timer, which then fires at once as a false timeout.
|
|
@@ -239,7 +398,7 @@ export class PipelexApiClient {
|
|
|
239
398
|
response = await fetch(url, {
|
|
240
399
|
method,
|
|
241
400
|
headers,
|
|
242
|
-
body:
|
|
401
|
+
body: bodyText,
|
|
243
402
|
signal: controller.signal,
|
|
244
403
|
});
|
|
245
404
|
// The body streams after the headers; keep the timer/abort armed until it
|
|
@@ -332,16 +491,46 @@ export class PipelexApiClient {
|
|
|
332
491
|
return withParsedPython(wire, (cause) => this.unreadableAnswer(method, `/${API_PREFIX}/${endpoint}`, res, `${subject} whose \`python\` field could not be read`, cause));
|
|
333
492
|
}
|
|
334
493
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* `
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
494
|
+
* A verdict, the `200` a route answers discriminated on `outcome` with the object the verdict
|
|
495
|
+
* is about as one `member`: `publishMethod`'s `method`, `recordMethodExampleLatestRun`'s
|
|
496
|
+
* `example`. The answer is read as `readObjectAnswer` reads it, then an `outcome` that is none
|
|
497
|
+
* of `outcomes` is an answer the SDK cannot read, and so is a `member` that is not an object,
|
|
498
|
+
* the check `readObjectAnswerAt` holds a route's whole answer to: each throws the
|
|
499
|
+
* `ApiResponseError` `unreadableAnswer` builds, `verdict` naming the answer in the message.
|
|
500
|
+
* A route whose member has a reader of its own passes it as `readMember`, which then checks the
|
|
501
|
+
* member in place of that object check, so the member is checked once. `parse` reads the
|
|
502
|
+
* answer's text, `readJson` unless the route names another. Returns the answer, its `outcome`
|
|
503
|
+
* and the member as `subject`, for the route to read further.
|
|
341
504
|
*/
|
|
342
|
-
|
|
505
|
+
readVerdict(method, endpoint, res, shape) {
|
|
506
|
+
const answer = this.readObjectAnswer(method, endpoint, res, {}, shape.parse);
|
|
507
|
+
const outcome = answer.outcome;
|
|
508
|
+
const subject = answer[shape.member];
|
|
509
|
+
if (!shape.outcomes.includes(outcome)) {
|
|
510
|
+
const expected = inWords(shape.outcomes.map((name) => `\`${name}\``), "or");
|
|
511
|
+
throw this.unreadableAnswer(method, `/${API_PREFIX}/${endpoint}`, res, `${shape.verdict} whose \`outcome\` is not ${expected}`);
|
|
512
|
+
}
|
|
513
|
+
if (shape.readMember !== undefined) {
|
|
514
|
+
return { answer, outcome, subject: shape.readMember(subject) };
|
|
515
|
+
}
|
|
516
|
+
if (!isPlainObject(subject)) {
|
|
517
|
+
throw this.unreadableAnswer(method, `/${API_PREFIX}/${endpoint}`, res, `${shape.verdict} whose \`${shape.member}\` is not an object`);
|
|
518
|
+
}
|
|
519
|
+
return { answer, outcome, subject: subject };
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* One page of a cursor-paged product route (`listMethods`, `listMethodVersions`,
|
|
523
|
+
* `listMethodExamples`, `listRuns`), read as the platform always serializes it: `items` an
|
|
524
|
+
* array, `next_cursor` a string or `null`. A page breaking either is an answer the SDK cannot
|
|
525
|
+
* read, so it throws the `ApiResponseError` `unreadableAnswer` builds, `runtime` and not
|
|
526
|
+
* retryable, rather than handing the iterators a page they cannot walk: `items` that is not
|
|
527
|
+
* iterable, or a missing cursor that is neither the end nor a next page, which `iterateRuns`
|
|
528
|
+
* would follow forever. A route reading its rows itself passes `rows`: `parse` reads the page's
|
|
529
|
+
* text, and `item` reads each row, throwing as `unreadableAnswer` does for one it cannot read.
|
|
530
|
+
*/
|
|
531
|
+
async requestPage(endpoint, rows) {
|
|
343
532
|
const res = await this.requestProductAnswer("GET", endpoint, undefined);
|
|
344
|
-
const page = this.readObjectAnswer("GET", endpoint, res);
|
|
533
|
+
const page = this.readObjectAnswer("GET", endpoint, res, {}, rows?.parse);
|
|
345
534
|
const { items, next_cursor: nextCursor } = page;
|
|
346
535
|
if (!Array.isArray(items)) {
|
|
347
536
|
throw this.unreadableAnswer("GET", `/${API_PREFIX}/${endpoint}`, res, "a page whose `items` is not an array");
|
|
@@ -349,7 +538,40 @@ export class PipelexApiClient {
|
|
|
349
538
|
if (nextCursor !== null && typeof nextCursor !== "string") {
|
|
350
539
|
throw this.unreadableAnswer("GET", `/${API_PREFIX}/${endpoint}`, res, "a page whose `next_cursor` is neither a string nor null");
|
|
351
540
|
}
|
|
352
|
-
|
|
541
|
+
if (rows === undefined)
|
|
542
|
+
return { items: items, nextCursor };
|
|
543
|
+
return { items: items.map((row) => rows.item(row, res)), nextCursor };
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* A route answering one method example (`createMethodExample`, `getMethodExample`,
|
|
547
|
+
* `updateMethodExample`), its answer read with `parseExactJson`, so an integer past the safe
|
|
548
|
+
* range in the inputs is a `bigint` holding its exact digits, and checked by
|
|
549
|
+
* `readMethodExample`.
|
|
550
|
+
*/
|
|
551
|
+
async requestMethodExample(method, endpoint, body) {
|
|
552
|
+
const res = await this.requestProductAnswer(method, endpoint, body);
|
|
553
|
+
const answer = this.readObjectAnswer(method, endpoint, res, {}, parseExactJson);
|
|
554
|
+
return this.readMethodExample(method, endpoint, res, answer, "an example");
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* A method example as `res` carried it, checked against `METHOD_EXAMPLE_FIELDS`: a value that
|
|
558
|
+
* is not an object, or that misses a field the Python SDK's model requires or holds one of
|
|
559
|
+
* another type, is an answer the SDK cannot read, thrown as the `ApiResponseError`
|
|
560
|
+
* `unreadableAnswer` builds, `where` naming the example's place in the answer in the message.
|
|
561
|
+
* Every route answering an example reads it here, the page rows and the latest-run verdict's
|
|
562
|
+
* member included.
|
|
563
|
+
*/
|
|
564
|
+
readMethodExample(method, endpoint, res, value, where) {
|
|
565
|
+
const path = `/${API_PREFIX}/${endpoint}`;
|
|
566
|
+
if (!isPlainObject(value)) {
|
|
567
|
+
throw this.unreadableAnswer(method, path, res, `${where} that is not an object`);
|
|
568
|
+
}
|
|
569
|
+
for (const [field, [expected, fits]] of Object.entries(METHOD_EXAMPLE_FIELDS)) {
|
|
570
|
+
if (!fits(value[field])) {
|
|
571
|
+
throw this.unreadableAnswer(method, path, res, `${where} whose \`${field}\` is not ${expected}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return value;
|
|
353
575
|
}
|
|
354
576
|
/** Issue a product request and return its 2xx answer, a non-2xx thrown as `ApiResponseError`. */
|
|
355
577
|
async requestProductAnswer(method, endpoint, body, options = {}) {
|
|
@@ -371,25 +593,35 @@ export class PipelexApiClient {
|
|
|
371
593
|
return this.readAnswerAt(method, `/${API_PREFIX}/${endpoint}`, res);
|
|
372
594
|
}
|
|
373
595
|
/** The JSON object body of a `/v1` route's 2xx answer; see `readObjectAnswerAt`. */
|
|
374
|
-
readObjectAnswer(method, endpoint, res, run = {}) {
|
|
375
|
-
return this.readObjectAnswerAt(method, `/${API_PREFIX}/${endpoint}`, res, run);
|
|
596
|
+
readObjectAnswer(method, endpoint, res, run = {}, parse = readJson) {
|
|
597
|
+
return this.readObjectAnswerAt(method, `/${API_PREFIX}/${endpoint}`, res, run, parse);
|
|
376
598
|
}
|
|
377
599
|
/**
|
|
378
600
|
* The JSON body of a 2xx answer, naming the route by its `path` from the origin as a refusal
|
|
379
|
-
* does
|
|
380
|
-
*
|
|
381
|
-
*
|
|
601
|
+
* does, its text read by `parse`: `JSON.parse`, or `parseExactJson` for an answer carrying a
|
|
602
|
+
* person's data. A body that is not UTF-8, or not JSON, an empty one included, is an answer the
|
|
603
|
+
* SDK cannot read, so it throws the `ApiResponseError` `unreadableAnswer` builds, with the parse
|
|
604
|
+
* failure as `cause`; so is a body holding a number `parseExactJson` cannot read exactly on this
|
|
605
|
+
* runtime (`ExactJsonUnsupportedError`), rather than read rounded.
|
|
382
606
|
* The one place a success body is parsed.
|
|
383
607
|
*/
|
|
384
|
-
readAnswerAt(method, path, res, run = {}) {
|
|
608
|
+
readAnswerAt(method, path, res, run = {}, parse = readJson) {
|
|
385
609
|
if (!res.utf8) {
|
|
386
610
|
throw this.unreadableAnswer(method, path, res, "a body that is not UTF-8", undefined, run);
|
|
387
611
|
}
|
|
388
612
|
try {
|
|
389
|
-
return
|
|
613
|
+
return parse(res.body);
|
|
390
614
|
}
|
|
391
615
|
catch (err) {
|
|
392
|
-
|
|
616
|
+
let what;
|
|
617
|
+
if (err instanceof ExactJsonUnsupportedError) {
|
|
618
|
+
what =
|
|
619
|
+
"a body holding a number past Number.MAX_SAFE_INTEGER, which this runtime cannot read " +
|
|
620
|
+
"exactly";
|
|
621
|
+
}
|
|
622
|
+
else
|
|
623
|
+
what = res.body ? "a body that is not JSON" : "an empty body where JSON was expected";
|
|
624
|
+
throw this.unreadableAnswer(method, path, res, what, err, run);
|
|
393
625
|
}
|
|
394
626
|
}
|
|
395
627
|
/**
|
|
@@ -397,8 +629,8 @@ export class PipelexApiClient {
|
|
|
397
629
|
* reads it. A body that is JSON but not an object — `null`, a number, a string, an array — is
|
|
398
630
|
* an answer the SDK cannot read either, and throws the same `ApiResponseError`, with no `cause`.
|
|
399
631
|
*/
|
|
400
|
-
readObjectAnswerAt(method, path, res, run = {}) {
|
|
401
|
-
const answer = this.readAnswerAt(method, path, res, run);
|
|
632
|
+
readObjectAnswerAt(method, path, res, run = {}, parse = readJson) {
|
|
633
|
+
const answer = this.readAnswerAt(method, path, res, run, parse);
|
|
402
634
|
if (!isPlainObject(answer)) {
|
|
403
635
|
throw this.unreadableAnswer(method, path, res, "a body that is not an object", undefined, run);
|
|
404
636
|
}
|
|
@@ -503,6 +735,9 @@ export class PipelexApiClient {
|
|
|
503
735
|
* that exceeds that surfaces as `PipelineExecuteTimeoutError` pointing at the
|
|
504
736
|
* durable start+poll path. Throws `RunStillRunningError` on the protocol's
|
|
505
737
|
* optional 202 degrade.
|
|
738
|
+
*
|
|
739
|
+
* The answer is read with `parseExactJson`, so an integer past `Number.MAX_SAFE_INTEGER` in the
|
|
740
|
+
* working memory, the main output included, is a `bigint` holding its exact digits.
|
|
506
741
|
*/
|
|
507
742
|
async execute(options) {
|
|
508
743
|
const extensions = buildExtensions(options.extra);
|
|
@@ -548,8 +783,9 @@ export class PipelexApiClient {
|
|
|
548
783
|
this.throwApiResponseError("POST", "execute", res, STARTS_RUN);
|
|
549
784
|
}
|
|
550
785
|
// Wrap the base result in the enriched subtype (adds the `.main_stuff` accessor; the
|
|
551
|
-
// `main_stuff_name` extension + working memory ride `pipe_output`).
|
|
552
|
-
|
|
786
|
+
// `main_stuff_name` extension + working memory ride `pipe_output`). Read exactly: the working
|
|
787
|
+
// memory holds the run's inputs and outputs.
|
|
788
|
+
const answer = this.readObjectAnswer("POST", "execute", res, STARTS_RUN, parseExactJson);
|
|
553
789
|
if (!isCompletedRunAnswer(answer)) {
|
|
554
790
|
throw this.unreadableAnswer("POST", `/${API_PREFIX}/execute`, res, "a body that is not the answer the route returns", undefined, STARTS_RUN);
|
|
555
791
|
}
|
|
@@ -1109,7 +1345,7 @@ export class PipelexApiClient {
|
|
|
1109
1345
|
* lifecycle routes are absent (a bare runner).
|
|
1110
1346
|
*/
|
|
1111
1347
|
async getRunStatus(runId, options = {}) {
|
|
1112
|
-
const endpoint = `${RUNS}/${
|
|
1348
|
+
const endpoint = `${RUNS}/${idPathSegment(runId, "getRunStatus", RUN_ID_ARGUMENT)}/status`;
|
|
1113
1349
|
const url = this.url(endpoint);
|
|
1114
1350
|
const res = await this.requestRaw("GET", url, {
|
|
1115
1351
|
timeoutMs: POLL_REQUEST_TIMEOUT_MS,
|
|
@@ -1142,12 +1378,17 @@ export class PipelexApiClient {
|
|
|
1142
1378
|
* before any request. `MissingMainStuffError` is thrown only for a read that asked
|
|
1143
1379
|
* for `main_stuff` — no selection, or one naming it.
|
|
1144
1380
|
*
|
|
1381
|
+
* A completed result is read with `parseExactJson`, so an integer past
|
|
1382
|
+
* `Number.MAX_SAFE_INTEGER` anywhere in it, the main output, the working memory and the graph
|
|
1383
|
+
* included, is a `bigint` holding its exact digits, and an output passed back to `start` goes
|
|
1384
|
+
* back as the run produced it.
|
|
1385
|
+
*
|
|
1145
1386
|
* Throws `RunLifecycleUnavailableError` when the lifecycle routes are absent
|
|
1146
1387
|
* (a bare runner).
|
|
1147
1388
|
*/
|
|
1148
1389
|
async getRunResult(runId, options = {}) {
|
|
1149
1390
|
assertArtifactSelection(options.artifacts);
|
|
1150
|
-
const base = `${RUNS}/${
|
|
1391
|
+
const base = `${RUNS}/${idPathSegment(runId, "getRunResult", RUN_ID_ARGUMENT)}/results`;
|
|
1151
1392
|
// Deduplicated, in the caller's order; the platform reads `a,b` as the set {a, b}. The names
|
|
1152
1393
|
// are the validated vocabulary above, so they need no escaping and the comma stays literal.
|
|
1153
1394
|
const endpoint = options.artifacts === undefined
|
|
@@ -1172,7 +1413,8 @@ export class PipelexApiClient {
|
|
|
1172
1413
|
if (res.status < 200 || res.status >= 300) {
|
|
1173
1414
|
this.throwApiResponseError("GET", endpoint, res);
|
|
1174
1415
|
}
|
|
1175
|
-
|
|
1416
|
+
// Read exactly: the output, the working memory and the graph hold the run's data.
|
|
1417
|
+
const result = this.readObjectAnswer("GET", endpoint, res, {}, parseExactJson);
|
|
1176
1418
|
if (selectionIncludesMainStuff(options.artifacts) && result.main_stuff == null) {
|
|
1177
1419
|
throw new MissingMainStuffError(`Completed run '${runId}' returned no main stuff — a completed run always delivers a main stuff.`, runId);
|
|
1178
1420
|
}
|
|
@@ -1315,22 +1557,13 @@ export class PipelexApiClient {
|
|
|
1315
1557
|
* come back empty and still carry a `nextCursor`: keep following it until it is `null`.
|
|
1316
1558
|
*/
|
|
1317
1559
|
async listMethods(query = {}) {
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
params.set("q", query.q);
|
|
1326
|
-
if (query.published !== undefined)
|
|
1327
|
-
params.set("published", String(query.published));
|
|
1328
|
-
if (query.limit !== undefined)
|
|
1329
|
-
params.set("limit", String(query.limit));
|
|
1330
|
-
if (query.cursor !== undefined)
|
|
1331
|
-
params.set("cursor", query.cursor);
|
|
1332
|
-
const suffix = params.toString();
|
|
1333
|
-
return this.requestPage(suffix ? `methods?${suffix}` : "methods");
|
|
1560
|
+
const search = productQuery({
|
|
1561
|
+
q: query.q,
|
|
1562
|
+
published: query.published,
|
|
1563
|
+
limit: query.limit,
|
|
1564
|
+
cursor: query.cursor,
|
|
1565
|
+
});
|
|
1566
|
+
return this.requestPage(`methods${search}`);
|
|
1334
1567
|
}
|
|
1335
1568
|
/**
|
|
1336
1569
|
* Every method in the catalog, streamed — follows the cursor for you.
|
|
@@ -1368,10 +1601,12 @@ export class PipelexApiClient {
|
|
|
1368
1601
|
* Takes a bare catalog id: the method routes address the method itself, never a version of
|
|
1369
1602
|
* it. A caller holding `mt_…@3` strips the suffix with `parseMethodSelector` and reads that
|
|
1370
1603
|
* version with `getMethodVersion`. Every method route throws `RequestArgumentError` for a
|
|
1371
|
-
* suffixed id, before any request, rather than read back the `404` the platform would answer
|
|
1604
|
+
* suffixed id, before any request, rather than read back the `404` the platform would answer,
|
|
1605
|
+
* and for an id that is empty, `.` or `..`, which the URL would resolve into another route's
|
|
1606
|
+
* path.
|
|
1372
1607
|
*/
|
|
1373
1608
|
async getMethod(methodId) {
|
|
1374
|
-
return this.requestMethodData("GET", methodPath(methodId));
|
|
1609
|
+
return this.requestMethodData("GET", methodPath(methodId, "getMethod"));
|
|
1375
1610
|
}
|
|
1376
1611
|
/**
|
|
1377
1612
|
* Resolve a stored method's id into the runnable MTHDS closure of its DRAFT — a
|
|
@@ -1438,7 +1673,7 @@ export class PipelexApiClient {
|
|
|
1438
1673
|
* write.
|
|
1439
1674
|
*/
|
|
1440
1675
|
async writeDraft(methodId, input) {
|
|
1441
|
-
return this.requestMethodData("PUT", `${methodPath(methodId)}/draft`, withWirePython(input));
|
|
1676
|
+
return this.requestMethodData("PUT", `${methodPath(methodId, "writeDraft")}/draft`, withWirePython(input));
|
|
1442
1677
|
}
|
|
1443
1678
|
/**
|
|
1444
1679
|
* Rename a method — `PATCH /v1/methods/{id}` — and return it. The name belongs to the method,
|
|
@@ -1453,7 +1688,7 @@ export class PipelexApiClient {
|
|
|
1453
1688
|
* writes kept landing under the rename.
|
|
1454
1689
|
*/
|
|
1455
1690
|
async renameMethod(methodId, input) {
|
|
1456
|
-
return this.requestMethodData("PATCH", methodPath(methodId), {
|
|
1691
|
+
return this.requestMethodData("PATCH", methodPath(methodId, "renameMethod"), {
|
|
1457
1692
|
name: input.name,
|
|
1458
1693
|
});
|
|
1459
1694
|
}
|
|
@@ -1495,19 +1730,16 @@ export class PipelexApiClient {
|
|
|
1495
1730
|
throw new RequestArgumentError("publishMethod() needs expected_draft_updated_at: the draft token (the method's " +
|
|
1496
1731
|
"updated_at) the caller last saw, so a publish never takes a draft it has not seen.");
|
|
1497
1732
|
}
|
|
1498
|
-
const endpoint = `${methodPath(methodId)}/publish`;
|
|
1733
|
+
const endpoint = `${methodPath(methodId, "publishMethod")}/publish`;
|
|
1499
1734
|
const res = await this.requestProductAnswer("POST", endpoint, {
|
|
1500
1735
|
expected_draft_updated_at: token,
|
|
1501
1736
|
});
|
|
1502
|
-
const answer = this.
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
throw this.unreadableAnswer("POST", `/${API_PREFIX}/${endpoint}`, res, "a publish result whose `method` is not an object");
|
|
1509
|
-
}
|
|
1510
|
-
const method = this.readStoredSources("POST", endpoint, res, stored, "a stored method");
|
|
1737
|
+
const { answer, outcome, subject } = this.readVerdict("POST", endpoint, res, {
|
|
1738
|
+
verdict: "a publish result",
|
|
1739
|
+
outcomes: ["published", "unchanged", "refused"],
|
|
1740
|
+
member: "method",
|
|
1741
|
+
});
|
|
1742
|
+
const method = this.readStoredSources("POST", endpoint, res, subject, "a stored method");
|
|
1511
1743
|
return { ...answer, outcome, method };
|
|
1512
1744
|
}
|
|
1513
1745
|
/**
|
|
@@ -1524,16 +1756,9 @@ export class PipelexApiClient {
|
|
|
1524
1756
|
* is an answer the SDK cannot read, thrown as an `ApiResponseError` too.
|
|
1525
1757
|
*/
|
|
1526
1758
|
async listMethodVersions(methodId, query = {}) {
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
if (query.limit !== undefined)
|
|
1531
|
-
params.set("limit", String(query.limit));
|
|
1532
|
-
if (query.cursor !== undefined)
|
|
1533
|
-
params.set("cursor", query.cursor);
|
|
1534
|
-
const suffix = params.toString();
|
|
1535
|
-
const endpoint = `${methodPath(methodId)}/versions`;
|
|
1536
|
-
return this.requestPage(suffix ? `${endpoint}?${suffix}` : endpoint);
|
|
1759
|
+
const search = productQuery({ limit: query.limit, cursor: query.cursor });
|
|
1760
|
+
const endpoint = `${methodPath(methodId, "listMethodVersions")}/versions`;
|
|
1761
|
+
return this.requestPage(`${endpoint}${search}`);
|
|
1537
1762
|
}
|
|
1538
1763
|
/**
|
|
1539
1764
|
* One published version of a method, with its sources —
|
|
@@ -1547,9 +1772,9 @@ export class PipelexApiClient {
|
|
|
1547
1772
|
*/
|
|
1548
1773
|
async getMethodVersion(methodId, version) {
|
|
1549
1774
|
if (!Number.isSafeInteger(version) || version < 1) {
|
|
1550
|
-
throw
|
|
1775
|
+
throw argumentRefusal("getMethodVersion", "a version number, a positive integer", describeNumberArgument(version));
|
|
1551
1776
|
}
|
|
1552
|
-
const endpoint = `${methodPath(methodId)}/versions/${version}`;
|
|
1777
|
+
const endpoint = `${methodPath(methodId, "getMethodVersion")}/versions/${version}`;
|
|
1553
1778
|
const res = await this.requestProductAnswer("GET", endpoint, undefined);
|
|
1554
1779
|
const wire = this.readObjectAnswer("GET", endpoint, res);
|
|
1555
1780
|
return this.readStoredSources("GET", endpoint, res, wire, "a published version");
|
|
@@ -1573,7 +1798,203 @@ export class PipelexApiClient {
|
|
|
1573
1798
|
* as `getMethod` does: the whole method is erased, never one of its versions.
|
|
1574
1799
|
*/
|
|
1575
1800
|
async deleteMethod(methodId) {
|
|
1576
|
-
return this.requestProduct("DELETE", methodPath(methodId));
|
|
1801
|
+
return this.requestProduct("DELETE", methodPath(methodId, "deleteMethod"));
|
|
1802
|
+
}
|
|
1803
|
+
// ── Method examples (`/v1/methods/{id}/examples`) ──────────────────────
|
|
1804
|
+
//
|
|
1805
|
+
// Every example route takes a bare catalog id, as `getMethod` does, and answers `404`
|
|
1806
|
+
// `not_found` for an unknown method or example and `409` `method_being_deleted` while the
|
|
1807
|
+
// method's erasure runs. The writes are `403` to a read-only key, which may still read.
|
|
1808
|
+
//
|
|
1809
|
+
// Every answer carrying an example is read with `parseExactJson`, so an integer past
|
|
1810
|
+
// `Number.MAX_SAFE_INTEGER` in its inputs, which another client may have saved, is a `bigint`
|
|
1811
|
+
// holding its exact digits, and every request body is written with `stringifyExactJson`, so
|
|
1812
|
+
// such inputs go back digit for digit, to `updateMethodExample` or as a run's inputs. Each
|
|
1813
|
+
// example is checked by `readMethodExample`: one missing a field the Python SDK's model
|
|
1814
|
+
// requires, or holding one of another type, is an answer the SDK cannot read.
|
|
1815
|
+
/**
|
|
1816
|
+
* Save an example on a method — `POST /v1/methods/{id}/examples`: a named set of inputs, and
|
|
1817
|
+
* with `run_id`, a run of this method that used exactly those inputs, recorded as its latest
|
|
1818
|
+
* run. Answers the example as stored, at `input_revision` 1.
|
|
1819
|
+
*
|
|
1820
|
+
* Sends `name`, `inputs` and, when given, `run_id`, and nothing else: the route's body is
|
|
1821
|
+
* closed, and a `null` `run_id` reads as not given. Throws `RequestArgumentError` before any
|
|
1822
|
+
* request when `name` or `inputs` is not given (`null` counting as not given), and for a
|
|
1823
|
+
* `run_id` that is given but is not a non-empty string. Throws `ApiResponseError`: `404`
|
|
1824
|
+
* `run_not_found` for a `run_id` that is not one of this method's runs in the caller's
|
|
1825
|
+
* organization, with nothing written; `413` `payload_too_large` for an example larger than one
|
|
1826
|
+
* stored item holds; `422` for a name that is empty, blank or over 200 characters, or inputs
|
|
1827
|
+
* that are not a JSON object; and the codes every example route answers.
|
|
1828
|
+
*/
|
|
1829
|
+
async createMethodExample(methodId, input) {
|
|
1830
|
+
const endpoint = `${methodPath(methodId, "createMethodExample")}/examples`;
|
|
1831
|
+
const { name, inputs, run_id: runId, } = input ?? {};
|
|
1832
|
+
if (name == null || inputs == null) {
|
|
1833
|
+
let got;
|
|
1834
|
+
if (name == null && inputs == null)
|
|
1835
|
+
got = "neither";
|
|
1836
|
+
else
|
|
1837
|
+
got = name == null ? "no name" : "no inputs";
|
|
1838
|
+
throw new RequestArgumentError(`createMethodExample() needs the example's name and its inputs; got ${got}.`);
|
|
1839
|
+
}
|
|
1840
|
+
const body = { name, inputs };
|
|
1841
|
+
if (runId != null) {
|
|
1842
|
+
body.run_id = requireNonEmptyId(runId, "createMethodExample", CREATE_RUN_ID_ARGUMENT);
|
|
1843
|
+
}
|
|
1844
|
+
return this.requestMethodExample("POST", endpoint, body);
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* One page of a method's examples, oldest first, inputs included —
|
|
1848
|
+
* `GET /v1/methods/{id}/examples`.
|
|
1849
|
+
*
|
|
1850
|
+
* Pass the page's `nextCursor` back as `cursor` to continue; `null` is the last page. A page
|
|
1851
|
+
* may be short while `nextCursor` is set, because the platform reads examples whole and stops
|
|
1852
|
+
* a read at 1 MB; `iterateMethodExamples` follows the cursor for a caller that wants them all.
|
|
1853
|
+
*
|
|
1854
|
+
* Throws `ApiResponseError`: `400` `malformed_request` for a cursor the listing did not issue
|
|
1855
|
+
* for this method, `422` for a `limit` outside 1 to 100, and the codes every example route
|
|
1856
|
+
* answers. A page whose `items` is not an array, whose `next_cursor` is neither a string nor
|
|
1857
|
+
* `null`, or one of whose items is not an example `readMethodExample` can read, is an answer
|
|
1858
|
+
* the SDK cannot read, thrown as an `ApiResponseError` too. An example whose inputs hold an
|
|
1859
|
+
* integer past the safe range is read like any other, that integer as a `bigint`.
|
|
1860
|
+
*/
|
|
1861
|
+
async listMethodExamples(methodId, query = {}) {
|
|
1862
|
+
const search = productQuery({ limit: query.limit, cursor: query.cursor });
|
|
1863
|
+
const endpoint = `${methodPath(methodId, "listMethodExamples")}/examples${search}`;
|
|
1864
|
+
return this.requestPage(endpoint, {
|
|
1865
|
+
parse: parseExactJson,
|
|
1866
|
+
item: (row, res) => this.readMethodExample("GET", endpoint, res, row, "a page item"),
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Every example of a method, oldest first, streamed — follows the cursor of
|
|
1871
|
+
* `listMethodExamples` for you.
|
|
1872
|
+
*
|
|
1873
|
+
* ```ts
|
|
1874
|
+
* for await (const example of client.iterateMethodExamples(methodId)) {
|
|
1875
|
+
* rerun(example); // stop whenever you like
|
|
1876
|
+
* }
|
|
1877
|
+
* ```
|
|
1878
|
+
*
|
|
1879
|
+
* An iterator rather than an all-at-once list, for the reason `iterateMethods` is one: a
|
|
1880
|
+
* helper returning an array needs a page cap, and a cap returns a truncated list with no
|
|
1881
|
+
* error. It runs the same loop as `iterateMethods` and `iterateRuns`, so it keeps going through
|
|
1882
|
+
* a short or empty page that carries a cursor, stops on a `null` cursor or on a cursor the
|
|
1883
|
+
* server failed to advance, and throws `PagingNotTerminatingError` past the same page ceiling.
|
|
1884
|
+
*
|
|
1885
|
+
* Throws `RequestArgumentError` at the call, before the iterator is pulled or anything is sent,
|
|
1886
|
+
* for a method id `listMethodExamples` refuses: a suffixed one, or one that is empty, `.` or `..`
|
|
1887
|
+
* or holds a lone surrogate.
|
|
1888
|
+
*/
|
|
1889
|
+
iterateMethodExamples(methodId, query = {}) {
|
|
1890
|
+
// Checked here rather than in a generator's body, which runs only once the iterator is pulled
|
|
1891
|
+
// and would refuse the id at the first pull, away from the call that passed it.
|
|
1892
|
+
methodPath(methodId, "iterateMethodExamples");
|
|
1893
|
+
return followCursor("listMethodExamples", (cursor) => this.listMethodExamples(methodId, { ...query, cursor }));
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* One example of a method — `GET /v1/methods/{id}/examples/{example_id}`, read consistently,
|
|
1897
|
+
* so an example saved a moment ago is found.
|
|
1898
|
+
*
|
|
1899
|
+
* Throws `RequestArgumentError` for an example id that is empty, `.` or `..`, or holds a lone
|
|
1900
|
+
* surrogate, before any request. Throws `ApiResponseError`: `422` for an example id longer
|
|
1901
|
+
* than one can be (64 characters), and the codes every example route answers.
|
|
1902
|
+
*/
|
|
1903
|
+
async getMethodExample(methodId, exampleId) {
|
|
1904
|
+
const endpoint = methodExamplePath(methodId, exampleId, "getMethodExample");
|
|
1905
|
+
return this.requestMethodExample("GET", endpoint);
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Rename an example, give it new inputs, or both —
|
|
1909
|
+
* `PATCH /v1/methods/{id}/examples/{example_id}` — and return it as it now stands.
|
|
1910
|
+
*
|
|
1911
|
+
* **New inputs raise `input_revision` by one and clear `latest_run_id`**, since the run no
|
|
1912
|
+
* longer shows what the inputs produce; inputs that are the same as the stored ones, their
|
|
1913
|
+
* keys in another order included, change nothing, while `3` becoming `3.0` is a change. A
|
|
1914
|
+
* rename alone keeps both. The edit takes no revision or token: the latest-run write is what
|
|
1915
|
+
* the revision guards. Sends `name` and `inputs` when given, and nothing else; `null` reads as
|
|
1916
|
+
* not given, as the platform reads it.
|
|
1917
|
+
*
|
|
1918
|
+
* **Send `inputs` only when they changed.** Inputs read from an example and sent back unchanged
|
|
1919
|
+
* are a change for the platform when they hold a float with no fractional part, `3.0`, which
|
|
1920
|
+
* JavaScript reads as `3` (see `MethodExample.inputs`): they raise the revision and clear the
|
|
1921
|
+
* latest run. A rename sent alone never does. An integer past `Number.MAX_SAFE_INTEGER` is no
|
|
1922
|
+
* such case: it reads as a `bigint` and is sent back as its exact digits.
|
|
1923
|
+
*
|
|
1924
|
+
* Throws `RequestArgumentError` before any request for an example id that is empty, `.` or
|
|
1925
|
+
* `..`, or when neither `name` nor `inputs` is given. Throws `ApiResponseError`: `413`
|
|
1926
|
+
* `payload_too_large` for inputs larger than one stored item holds, `422` for a name that is
|
|
1927
|
+
* empty, blank or over 200 characters, or inputs that are not a JSON object, a `503` that wrote
|
|
1928
|
+
* nothing, safe to retry, when concurrent edits kept landing under the write, and the codes
|
|
1929
|
+
* every example route answers.
|
|
1930
|
+
*/
|
|
1931
|
+
async updateMethodExample(methodId, exampleId, input) {
|
|
1932
|
+
const endpoint = methodExamplePath(methodId, exampleId, "updateMethodExample");
|
|
1933
|
+
const { name, inputs } = input ?? {};
|
|
1934
|
+
const body = {};
|
|
1935
|
+
if (name != null)
|
|
1936
|
+
body.name = name;
|
|
1937
|
+
if (inputs != null)
|
|
1938
|
+
body.inputs = inputs;
|
|
1939
|
+
if (Object.keys(body).length === 0) {
|
|
1940
|
+
throw new RequestArgumentError("updateMethodExample() needs a new name, new inputs, or both; got neither.");
|
|
1941
|
+
}
|
|
1942
|
+
return this.requestMethodExample("PATCH", endpoint, body);
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Record a run as an example's latest —
|
|
1946
|
+
* `PUT /v1/methods/{id}/examples/{example_id}/latest-run` — **only while the example still
|
|
1947
|
+
* holds the revision whose inputs the run used**.
|
|
1948
|
+
*
|
|
1949
|
+
* `input_revision` is the example's `input_revision` the caller read when it started the run
|
|
1950
|
+
* with the example's inputs; the platform trusts it and does not compare the run's own inputs.
|
|
1951
|
+
* The answer is a `MethodExampleLatestRunResult` discriminated on `outcome` — branch on it:
|
|
1952
|
+
* `recorded`, the run is now the example's latest; `stale`, the example's inputs were edited
|
|
1953
|
+
* since and nothing was written. A stale run is the expected end of a run that outlasted an
|
|
1954
|
+
* edit, so it is answered, never thrown. Both arms carry the `example` as it stands.
|
|
1955
|
+
*
|
|
1956
|
+
* Throws `RequestArgumentError` before any request, checking in this order: an example id that
|
|
1957
|
+
* is empty, `.` or `..`, or holds a lone surrogate, a `run_id` that is not a non-empty string,
|
|
1958
|
+
* and an `input_revision` that is not a positive integer. Throws `ApiResponseError` when no
|
|
1959
|
+
* verdict was produced: `404` `not_found` for an example deleted since, `404` `run_not_found`
|
|
1960
|
+
* for a run that is not one of this method's, with nothing written, and the codes every example
|
|
1961
|
+
* route answers. An answer whose `outcome` is neither arm's, or whose `example` is not an
|
|
1962
|
+
* example `readMethodExample` can read, is an answer the SDK cannot read, thrown as an
|
|
1963
|
+
* `ApiResponseError` too, read as `publishMethod` reads its verdict.
|
|
1964
|
+
*/
|
|
1965
|
+
async recordMethodExampleLatestRun(methodId, exampleId, input) {
|
|
1966
|
+
const call = "recordMethodExampleLatestRun";
|
|
1967
|
+
const endpoint = `${methodExamplePath(methodId, exampleId, call)}/latest-run`;
|
|
1968
|
+
const { run_id: runId, input_revision: revision } = input ?? {};
|
|
1969
|
+
const run = requireNonEmptyId(runId, call, RECORD_RUN_ID_ARGUMENT);
|
|
1970
|
+
if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1) {
|
|
1971
|
+
throw argumentRefusal(call, "an input_revision, the example's revision whose inputs the run used, a positive integer", describeNumberArgument(revision));
|
|
1972
|
+
}
|
|
1973
|
+
const res = await this.requestProductAnswer("PUT", endpoint, {
|
|
1974
|
+
run_id: run,
|
|
1975
|
+
input_revision: revision,
|
|
1976
|
+
});
|
|
1977
|
+
const { answer, outcome, subject } = this.readVerdict("PUT", endpoint, res, {
|
|
1978
|
+
verdict: "a latest-run result",
|
|
1979
|
+
outcomes: ["recorded", "stale"],
|
|
1980
|
+
member: "example",
|
|
1981
|
+
parse: parseExactJson,
|
|
1982
|
+
// Checked once, by the check every example the client hands back goes through.
|
|
1983
|
+
readMember: (value) => this.readMethodExample("PUT", endpoint, res, value, "a latest-run result's `example`"),
|
|
1984
|
+
});
|
|
1985
|
+
return { ...answer, outcome, example: subject };
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Delete an example — `DELETE /v1/methods/{id}/examples/{example_id}`. The platform answers
|
|
1989
|
+
* `204`. Deleting the example leaves its latest run in the method's history.
|
|
1990
|
+
*
|
|
1991
|
+
* Throws `RequestArgumentError` for an example id that is empty, `.` or `..`, before any
|
|
1992
|
+
* request: `..` would address the method, and its erasure would answer the delete. Throws
|
|
1993
|
+
* `ApiResponseError`: `404` `not_found` when there is no such example, one already deleted
|
|
1994
|
+
* included, and the codes every example route answers.
|
|
1995
|
+
*/
|
|
1996
|
+
async deleteMethodExample(methodId, exampleId) {
|
|
1997
|
+
await this.requestProduct("DELETE", methodExamplePath(methodId, exampleId, "deleteMethodExample"));
|
|
1577
1998
|
}
|
|
1578
1999
|
/** The caller's org memberships + active-org feature flags — `GET /v1/organizations/memberships`. */
|
|
1579
2000
|
async listMemberships() {
|
|
@@ -1585,7 +2006,8 @@ export class PipelexApiClient {
|
|
|
1585
2006
|
}
|
|
1586
2007
|
/** Rename an organization — `PATCH /v1/organizations/{org_id}`. */
|
|
1587
2008
|
async renameOrganization(orgId, input) {
|
|
1588
|
-
|
|
2009
|
+
const segment = idPathSegment(orgId, "renameOrganization", ORG_ID_ARGUMENT);
|
|
2010
|
+
return this.requestProduct("PATCH", `organizations/${segment}`, input);
|
|
1589
2011
|
}
|
|
1590
2012
|
/** The active org's subscription state — `GET /v1/billing/subscription`. */
|
|
1591
2013
|
async getSubscription() {
|
|
@@ -1632,14 +2054,16 @@ export class PipelexApiClient {
|
|
|
1632
2054
|
}
|
|
1633
2055
|
/** Revoke a Pipelex API key — `DELETE /v1/pipelex-api-keys/{id}`. */
|
|
1634
2056
|
async revokePipelexApiKey(id) {
|
|
1635
|
-
|
|
2057
|
+
const segment = idPathSegment(id, "revokePipelexApiKey", API_KEY_ID_ARGUMENT);
|
|
2058
|
+
await this.requestProduct("DELETE", `pipelex-api-keys/${segment}`);
|
|
1636
2059
|
}
|
|
1637
2060
|
/**
|
|
1638
2061
|
* Rotate a Pipelex API key — `POST /v1/pipelex-api-keys/{id}/rotate` (no
|
|
1639
2062
|
* body). Returns the new plaintext `api_key` once; the old key stops working.
|
|
1640
2063
|
*/
|
|
1641
2064
|
async rotatePipelexApiKey(id) {
|
|
1642
|
-
|
|
2065
|
+
const segment = idPathSegment(id, "rotatePipelexApiKey", API_KEY_ID_ARGUMENT);
|
|
2066
|
+
return this.requestProduct("POST", `pipelex-api-keys/${segment}/rotate`);
|
|
1643
2067
|
}
|
|
1644
2068
|
/** Submit the onboarding questionnaire — `POST /v1/onboarding/submit`. */
|
|
1645
2069
|
async submitOnboarding(input) {
|
|
@@ -1802,25 +2226,18 @@ export class PipelexApiClient {
|
|
|
1802
2226
|
* `method_version`.
|
|
1803
2227
|
*/
|
|
1804
2228
|
async listRuns(methodId, query = {}) {
|
|
1805
|
-
|
|
1806
|
-
//
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
for (const status of runStatusFilter(query.status) ?? [])
|
|
1818
|
-
params.append("status", status);
|
|
1819
|
-
if (query.limit !== undefined)
|
|
1820
|
-
params.set("limit", String(query.limit));
|
|
1821
|
-
if (query.cursor !== undefined)
|
|
1822
|
-
params.set("cursor", query.cursor);
|
|
1823
|
-
const page = await this.requestPage(`runs?${params.toString()}`);
|
|
2229
|
+
// An empty date is sent, not dropped: it reaches the API's instant parse and comes back a
|
|
2230
|
+
// `400` saying so, where dropping it would return every run (`productQuery`).
|
|
2231
|
+
const search = productQuery({
|
|
2232
|
+
method_id: methodId,
|
|
2233
|
+
created_from: query.createdFrom,
|
|
2234
|
+
created_to: query.createdTo,
|
|
2235
|
+
created_by: query.createdBy,
|
|
2236
|
+
status: runStatusFilter(query.status),
|
|
2237
|
+
limit: query.limit,
|
|
2238
|
+
cursor: query.cursor,
|
|
2239
|
+
});
|
|
2240
|
+
const page = await this.requestPage(`runs${search}`);
|
|
1824
2241
|
return { items: page.items.map(withCheckedReport), nextCursor: page.nextCursor };
|
|
1825
2242
|
}
|
|
1826
2243
|
/**
|
|
@@ -1862,13 +2279,20 @@ export class PipelexApiClient {
|
|
|
1862
2279
|
* executed) and `inputs`. Kept off the status read, which pollers hit every
|
|
1863
2280
|
* few seconds. A failed run's stored report, `error`, is checked field by
|
|
1864
2281
|
* field (see `RunErrorReport`).
|
|
2282
|
+
*
|
|
2283
|
+
* Read with `parseExactJson`, so an integer past `Number.MAX_SAFE_INTEGER` in the inputs is a
|
|
2284
|
+
* `bigint` holding its exact digits, and the inputs passed back to `start` or saved as an
|
|
2285
|
+
* example's go back as the run used them.
|
|
1865
2286
|
*/
|
|
1866
2287
|
async getRunDetail(runId) {
|
|
1867
|
-
|
|
2288
|
+
const endpoint = `runs/${idPathSegment(runId, "getRunDetail", RUN_ID_ARGUMENT)}`;
|
|
2289
|
+
const res = await this.requestProductAnswer("GET", endpoint, undefined);
|
|
2290
|
+
return withCheckedReport(this.readObjectAnswer("GET", endpoint, res, {}, parseExactJson));
|
|
1868
2291
|
}
|
|
1869
2292
|
/** Patch a run's status (admin/manual) — `PUT /v1/runs/{id}`. */
|
|
1870
2293
|
async updateRun(runId, input) {
|
|
1871
|
-
|
|
2294
|
+
const segment = idPathSegment(runId, "updateRun", RUN_ID_ARGUMENT);
|
|
2295
|
+
await this.requestProduct("PUT", `runs/${segment}`, input);
|
|
1872
2296
|
}
|
|
1873
2297
|
/**
|
|
1874
2298
|
* Blocking `POST /v1/execute` adapted onto `RunResults` — the bare-runner
|
|
@@ -1956,10 +2380,7 @@ function describeRefusedBaseUrl(value) {
|
|
|
1956
2380
|
const shown = `"${parsed.protocol}//${parsed.host}"`;
|
|
1957
2381
|
if (parts.length === 0)
|
|
1958
2382
|
return shown;
|
|
1959
|
-
|
|
1960
|
-
? parts[0]
|
|
1961
|
-
: `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
|
|
1962
|
-
return `${shown} with ${named} (not shown)`;
|
|
2383
|
+
return `${shown} with ${inWords(parts, "and")} (not shown)`;
|
|
1963
2384
|
}
|
|
1964
2385
|
// The protocol's own request fields — `extra` is for extension args only.
|
|
1965
2386
|
// `files` / `bundle_b64` are reserved too: they are named run-source options,
|