@tlockney/cdp-kernel 0.10.0 → 0.12.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/dist/browser/kernel.js +8 -2
- package/dist/browser/kernel.js.map +2 -2
- package/dist/browser/mod.d.ts +11 -0
- package/dist/browser/mod.js +11 -0
- package/dist/browser/target.d.ts +10 -0
- package/dist/browser/target.js +1 -1
- package/dist/browser/worker.js +1121 -77
- package/dist/browser/worker.js.map +4 -4
- package/dist/package.d.ts +3 -3
- package/dist/package.js +6 -2
- package/dist/workerd-target.js +1 -1
- package/package.json +1 -1
- package/runtime/browser-worker.mjs +6 -3
- package/runtime/cdn.d.mts +6 -1
- package/runtime/cdn.mjs +7 -2
- package/runtime/chart.d.mts +23 -0
- package/runtime/chart.mjs +359 -0
- package/runtime/display.d.mts +193 -68
- package/runtime/display.mjs +47 -67
- package/runtime/host.d.mts +7 -5
- package/runtime/sql.d.mts +147 -0
- package/runtime/sql.mjs +426 -0
- package/runtime/table-paging.d.mts +19 -0
- package/runtime/table-paging.mjs +183 -0
- package/runtime/table.d.mts +53 -0
- package/runtime/table.mjs +495 -0
- package/runtime/widget.d.mts +3 -3
package/runtime/display.d.mts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { CommApi, Comms } from "./comm.d.mts";
|
|
2
2
|
import type { Host, HostCallOptions, HostError, HostErrorCode, HostNotebooks, HostOptions, HostSelf, NotebookEntry, NotebookHit, NotebookSearch } from "./host.d.mts";
|
|
3
3
|
import type { Widget, WidgetOptions } from "./widget.d.mts";
|
|
4
|
+
import type { Sql, SqlColumn, SqlFragment, SqlResult } from "./sql.d.mts";
|
|
4
5
|
|
|
6
|
+
export type { Sql, SqlColumn, SqlFragment, SqlResult };
|
|
5
7
|
export type { CommApi, Host, HostCallOptions, HostError, HostErrorCode, HostNotebooks, HostOptions, HostSelf, NotebookEntry, NotebookHit, NotebookSearch, Widget, WidgetOptions };
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -34,6 +36,25 @@ export const RESULT_MIME: "application/vnd.ganymede.result+json";
|
|
|
34
36
|
/** A JSON value, as `JSON.parse` returns it. */
|
|
35
37
|
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
36
38
|
|
|
39
|
+
/** Options for `jupyter.table`. */
|
|
40
|
+
export interface TableOptions {
|
|
41
|
+
/** Rows to embed: default 1,000, at most 10,000, and never past 1 MB of JSON. */
|
|
42
|
+
limit?: number;
|
|
43
|
+
/** The columns to show, in order. Names an array of arrays' columns. */
|
|
44
|
+
columns?: string[];
|
|
45
|
+
/** A caption for the table. */
|
|
46
|
+
title?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Options for `jupyter.chart` with a Vega-Lite or Vega spec. */
|
|
50
|
+
export interface ChartOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Also keep the spec under its Vega MIME type, for a frontend that
|
|
53
|
+
* draws Vega itself. Default `false`: the SVG and a text summary only.
|
|
54
|
+
*/
|
|
55
|
+
spec?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
/** Options for `jupyter.display`. */
|
|
38
59
|
export interface DisplayOptions {
|
|
39
60
|
/**
|
|
@@ -62,33 +83,6 @@ export interface Displayable {
|
|
|
62
83
|
* as `Deno.jupyter` in `deno jupyter`.
|
|
63
84
|
*/
|
|
64
85
|
export interface JupyterNamespace {
|
|
65
|
-
/**
|
|
66
|
-
* Send a raw Jupyter message on the kernel's output channel (IOPub), as a
|
|
67
|
-
* reply to the running cell. The other helpers are built on it. It does
|
|
68
|
-
* not check the message: a wrong field is ignored or mishandled by the
|
|
69
|
-
* frontend.
|
|
70
|
-
*
|
|
71
|
-
* @param msgType An IOPub message type, such as `"clear_output"` or
|
|
72
|
-
* `"display_data"`.
|
|
73
|
-
* @param content The message content, as the messaging spec defines it
|
|
74
|
-
* for `msgType`.
|
|
75
|
-
* @param options `metadata` for the message, and binary `buffers`.
|
|
76
|
-
* @example
|
|
77
|
-
* ```ts
|
|
78
|
-
* // Count down in place: clear_output with wait avoids a flicker.
|
|
79
|
-
* for (const n of [3, 2, 1]) {
|
|
80
|
-
* jupyter.broadcast("clear_output", { wait: true });
|
|
81
|
-
* await jupyter.display(jupyter.md`Launching in **${n}**…`);
|
|
82
|
-
* await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
83
|
-
* }
|
|
84
|
-
* ```
|
|
85
|
-
* @see jupyter-api.md#raw-messages-with-broadcast
|
|
86
|
-
*/
|
|
87
|
-
broadcast(
|
|
88
|
-
msgType: string,
|
|
89
|
-
content: object,
|
|
90
|
-
options?: { metadata?: object; buffers?: Uint8Array[] },
|
|
91
|
-
): void;
|
|
92
86
|
/**
|
|
93
87
|
* Show a value now, in the middle of a cell, or replace one shown
|
|
94
88
|
* earlier. The value can be anything a cell could end with: a template
|
|
@@ -121,22 +115,6 @@ export interface JupyterNamespace {
|
|
|
121
115
|
* @see jupyter-api.md#showing-output-with-display
|
|
122
116
|
*/
|
|
123
117
|
display(obj: unknown, options?: DisplayOptions): Promise<void>;
|
|
124
|
-
/**
|
|
125
|
-
* The MIME bundle that displaying a value would send, without showing
|
|
126
|
-
* anything. For plain values `text/plain` holds terminal color codes,
|
|
127
|
-
* as the notebook shows them.
|
|
128
|
-
*
|
|
129
|
-
* @param obj The value to format.
|
|
130
|
-
* @returns The bundle, for example `{ "text/plain": "\x1b[33m42\x1b[39m" }`
|
|
131
|
-
* for `42`.
|
|
132
|
-
* @example
|
|
133
|
-
* ```ts
|
|
134
|
-
* const bundle = await jupyter.format(jupyter.md`# Weekly report`);
|
|
135
|
-
* bundle["text/markdown"]; // "# Weekly report"
|
|
136
|
-
* ```
|
|
137
|
-
* @see jupyter-api.md#getting-a-bundle-with-format
|
|
138
|
-
*/
|
|
139
|
-
format(obj: unknown): Promise<MediaBundle>;
|
|
140
118
|
/**
|
|
141
119
|
* A tagged template that renders as Markdown. Interpolated values are
|
|
142
120
|
* inserted as is, without escaping.
|
|
@@ -228,6 +206,175 @@ export interface JupyterNamespace {
|
|
|
228
206
|
* @see jupyter-api.md#custom-display-for-your-own-classes
|
|
229
207
|
*/
|
|
230
208
|
$display: typeof $display;
|
|
209
|
+
/**
|
|
210
|
+
* The MIME bundle that displaying a value would send, without showing
|
|
211
|
+
* anything. For plain values `text/plain` holds terminal color codes,
|
|
212
|
+
* as the notebook shows them.
|
|
213
|
+
*
|
|
214
|
+
* @param obj The value to format.
|
|
215
|
+
* @returns The bundle, for example `{ "text/plain": "\x1b[33m42\x1b[39m" }`
|
|
216
|
+
* for `42`.
|
|
217
|
+
* @example
|
|
218
|
+
* ```ts
|
|
219
|
+
* const bundle = await jupyter.format(jupyter.md`# Weekly report`);
|
|
220
|
+
* bundle["text/markdown"]; // "# Weekly report"
|
|
221
|
+
* ```
|
|
222
|
+
* @see jupyter-api.md#getting-a-bundle-with-format
|
|
223
|
+
*/
|
|
224
|
+
format(obj: unknown): Promise<MediaBundle>;
|
|
225
|
+
/**
|
|
226
|
+
* Send a raw Jupyter message on the kernel's output channel (IOPub), as a
|
|
227
|
+
* reply to the running cell. The other helpers are built on it. It does
|
|
228
|
+
* not check the message: a wrong field is ignored or mishandled by the
|
|
229
|
+
* frontend.
|
|
230
|
+
*
|
|
231
|
+
* @param msgType An IOPub message type, such as `"clear_output"` or
|
|
232
|
+
* `"display_data"`.
|
|
233
|
+
* @param content The message content, as the messaging spec defines it
|
|
234
|
+
* for `msgType`.
|
|
235
|
+
* @param options `metadata` for the message, and binary `buffers`.
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* // Count down in place: clear_output with wait avoids a flicker.
|
|
239
|
+
* for (const n of [3, 2, 1]) {
|
|
240
|
+
* jupyter.broadcast("clear_output", { wait: true });
|
|
241
|
+
* await jupyter.display(jupyter.md`Launching in **${n}**…`);
|
|
242
|
+
* await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
243
|
+
* }
|
|
244
|
+
* ```
|
|
245
|
+
* @see jupyter-api.md#raw-messages-with-broadcast
|
|
246
|
+
*/
|
|
247
|
+
broadcast(
|
|
248
|
+
msgType: string,
|
|
249
|
+
content: object,
|
|
250
|
+
options?: { metadata?: object; buffers?: Uint8Array[] },
|
|
251
|
+
): void;
|
|
252
|
+
/**
|
|
253
|
+
* Show rows and columns as a table. The output has Ganymede's table
|
|
254
|
+
* type, `application/vnd.dataresource+json` (Table Schema fields, up to
|
|
255
|
+
* `limit` rows as JSON, and the total row count), which the Ganymede
|
|
256
|
+
* apps show with sorting and paging; a plain HTML table of the first
|
|
257
|
+
* and last rows for JupyterLab and VS Code; and a text summary of each
|
|
258
|
+
* column's type and range for agents and consoles.
|
|
259
|
+
*
|
|
260
|
+
* An Arrow table, a Polars DataFrame or a DuckDB result also shows this
|
|
261
|
+
* way when it is a cell's value; pass one here to set `limit`,
|
|
262
|
+
* `columns` or `title`.
|
|
263
|
+
*
|
|
264
|
+
* @param value An array of row objects, an array of arrays (name them
|
|
265
|
+
* with `columns`), an Arrow table (apache-arrow, duckdb-wasm), a
|
|
266
|
+
* Polars DataFrame, or a `@duckdb/node-api` result.
|
|
267
|
+
* @param options `limit`: rows to embed, default 1,000, at most 10,000,
|
|
268
|
+
* and never more than 1 MB of JSON. `columns`: which columns, in order.
|
|
269
|
+
* `title`: a caption.
|
|
270
|
+
* @throws {TypeError} `value` isn't one of those, or `limit` isn't a
|
|
271
|
+
* whole number.
|
|
272
|
+
* @example
|
|
273
|
+
* ```ts
|
|
274
|
+
* const moons = [
|
|
275
|
+
* { moon: "Io", radius_km: 1821.6, discovered: new Date("1610-01-08") },
|
|
276
|
+
* { moon: "Europa", radius_km: 1560.8, discovered: new Date("1610-01-08") },
|
|
277
|
+
* { moon: "Ganymede", radius_km: 2634.1, discovered: new Date("1610-01-07") },
|
|
278
|
+
* ];
|
|
279
|
+
* jupyter.table(moons, { title: "Galilean moons" });
|
|
280
|
+
* ```
|
|
281
|
+
* @see jupyter-api.md#tables
|
|
282
|
+
*/
|
|
283
|
+
table(value: unknown, options?: TableOptions): Displayable;
|
|
284
|
+
/**
|
|
285
|
+
* Run SQL on an in-memory [DuckDB](https://duckdb.org) database, as a
|
|
286
|
+
* tagged template: each `${value}` becomes a bound parameter, never
|
|
287
|
+
* query text, so values can't change the query. Resolves to a
|
|
288
|
+
* `SqlResult`, which shows as a table and has `columns` and `rows`.
|
|
289
|
+
*
|
|
290
|
+
* The database opens on the first query and lasts until the kernel
|
|
291
|
+
* restarts or `jupyter.sql.close()`. DuckDB reads CSV, Parquet and JSON
|
|
292
|
+
* files by path or URL; `jupyter.sql.load` makes a table from rows the
|
|
293
|
+
* notebook already has. An interrupt stops a running query.
|
|
294
|
+
*
|
|
295
|
+
* @throws {TypeError} Called as a function rather than a tag.
|
|
296
|
+
* @availability Node, Deno and Bun use `@duckdb/node-api`; the browser
|
|
297
|
+
* target (the Ganymede apps) uses duckdb-wasm from a CDN. Not on
|
|
298
|
+
* workerd or celld.
|
|
299
|
+
* @example
|
|
300
|
+
* ```ts
|
|
301
|
+
* const min = 2000;
|
|
302
|
+
* await jupyter.sql`
|
|
303
|
+
* select moon, radius_km
|
|
304
|
+
* from (values ('Io', 1821.6), ('Ganymede', 2634.1), ('Callisto', 2410.3)) as m(moon, radius_km)
|
|
305
|
+
* where radius_km > ${min}
|
|
306
|
+
* order by radius_km desc`;
|
|
307
|
+
* ```
|
|
308
|
+
* @see jupyter-api.md#sql-with-duckdb
|
|
309
|
+
*/
|
|
310
|
+
readonly sql: Sql;
|
|
311
|
+
/**
|
|
312
|
+
* Draw a chart in the kernel as one SVG, with its title and legend
|
|
313
|
+
* inside, plus a text summary for readers that can't see it. The SVG
|
|
314
|
+
* shows the same in the Ganymede apps, JupyterLab, VS Code and a
|
|
315
|
+
* published page. Text and axes are drawn in `currentColor`: the
|
|
316
|
+
* notebook's text color in the Ganymede apps, black in JupyterLab.
|
|
317
|
+
*
|
|
318
|
+
* Pass a [Vega-Lite](https://vega.github.io/vega-lite/) or Vega spec
|
|
319
|
+
* (or a vega-lite-api chart), or the
|
|
320
|
+
* [Observable Plot](https://observablehq.com/plot/) module and the
|
|
321
|
+
* options you would give `Plot.plot`. Plot's `title`, `subtitle`,
|
|
322
|
+
* `caption` and the color legend (`legend: true`, `"swatches"` or
|
|
323
|
+
* `"ramp"`) are drawn into the SVG; other scales' legends are left
|
|
324
|
+
* out. An SVG element shows as it is.
|
|
325
|
+
*
|
|
326
|
+
* vega and vega-lite (version 6), and linkedom for Plot where there is
|
|
327
|
+
* no DOM, load through the cell's import on first use. A chart past
|
|
328
|
+
* 2 MB of SVG logs a warning: aggregate or sample first.
|
|
329
|
+
*
|
|
330
|
+
* @param chart A Vega-Lite or Vega spec, the Plot module, or an SVG
|
|
331
|
+
* element.
|
|
332
|
+
* @param options With a spec, `ChartOptions`; with Plot, Plot's options.
|
|
333
|
+
* @throws {TypeError} `chart` is none of those, or Plot comes without
|
|
334
|
+
* options.
|
|
335
|
+
* @example
|
|
336
|
+
* ```ts
|
|
337
|
+
* jupyter.chart({
|
|
338
|
+
* title: "Galilean moons",
|
|
339
|
+
* data: { values: [
|
|
340
|
+
* { moon: "Io", radius_km: 1821.6 },
|
|
341
|
+
* { moon: "Europa", radius_km: 1560.8 },
|
|
342
|
+
* { moon: "Ganymede", radius_km: 2634.1 },
|
|
343
|
+
* { moon: "Callisto", radius_km: 2410.3 },
|
|
344
|
+
* ] },
|
|
345
|
+
* mark: "bar",
|
|
346
|
+
* encoding: {
|
|
347
|
+
* x: { field: "moon", sort: "-y" },
|
|
348
|
+
* y: { field: "radius_km", type: "quantitative" },
|
|
349
|
+
* },
|
|
350
|
+
* });
|
|
351
|
+
* ```
|
|
352
|
+
* @see jupyter-api.md#charts
|
|
353
|
+
*/
|
|
354
|
+
chart(chart: object, options?: ChartOptions | Record<string, unknown>): Displayable;
|
|
355
|
+
/**
|
|
356
|
+
* An `AbortSignal` that an interrupt aborts, so code waiting on something
|
|
357
|
+
* only it can end can stop and clean up. Each cell gets its own, as does
|
|
358
|
+
* a comm handler that starts while no cell runs. Read it once when the
|
|
359
|
+
* work starts: a later read gets the signal of whatever started last.
|
|
360
|
+
* The abort reason is a `DOMException` named `AbortError`.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* ```ts
|
|
364
|
+
* const response = await fetch("https://example.com/large.csv", { signal: jupyter.signal });
|
|
365
|
+
* ```
|
|
366
|
+
* @example
|
|
367
|
+
* ```ts
|
|
368
|
+
* const signal = jupyter.signal;
|
|
369
|
+
* const ask = jupyter.comm.open("my.app", { question: "ready?" });
|
|
370
|
+
* const answer = await new Promise((resolve, reject) => {
|
|
371
|
+
* ask.onMsg((m) => resolve(m.data));
|
|
372
|
+
* signal.addEventListener("abort", () => { ask.close(); reject(signal.reason); }, { once: true });
|
|
373
|
+
* });
|
|
374
|
+
* ```
|
|
375
|
+
* @see jupyter-api.md#stopping-on-interrupt-jupyter-signal
|
|
376
|
+
*/
|
|
377
|
+
readonly signal: AbortSignal;
|
|
231
378
|
/**
|
|
232
379
|
* Jupyter comms: message channels to the frontend outside cell
|
|
233
380
|
* execution. Its methods are in the widgets and comms reference.
|
|
@@ -236,13 +383,14 @@ export interface JupyterNamespace {
|
|
|
236
383
|
*/
|
|
237
384
|
comm: CommApi;
|
|
238
385
|
/**
|
|
239
|
-
* Create an anywidget-compatible widget
|
|
386
|
+
* Create an [anywidget](https://anywidget.dev)-compatible [widget
|
|
387
|
+
* model](widgets-reference.md#widget-model). Display it (or end the
|
|
240
388
|
* cell with it) to show a view.
|
|
241
389
|
*
|
|
242
390
|
* @param options The view module, its CSS, and the initial state.
|
|
243
391
|
* @returns The model, synced with every view of it.
|
|
244
392
|
* @throws {TypeError} `options.esm` is not a string.
|
|
245
|
-
* @availability The frontend needs the anywidget extension.
|
|
393
|
+
* @availability The frontend needs the [anywidget](https://anywidget.dev) extension.
|
|
246
394
|
* @example
|
|
247
395
|
* ```ts
|
|
248
396
|
* const counter = jupyter.widget({
|
|
@@ -269,29 +417,6 @@ export interface JupyterNamespace {
|
|
|
269
417
|
* @see widgets.md#a-first-widget-a-counter
|
|
270
418
|
*/
|
|
271
419
|
widget(options: WidgetOptions): Widget;
|
|
272
|
-
/**
|
|
273
|
-
* An `AbortSignal` that an interrupt aborts, so code waiting on something
|
|
274
|
-
* only it can end can stop and clean up. Each cell gets its own, as does
|
|
275
|
-
* a comm handler that starts while no cell runs. Read it once when the
|
|
276
|
-
* work starts: a later read gets the signal of whatever started last.
|
|
277
|
-
* The abort reason is a `DOMException` named `AbortError`.
|
|
278
|
-
*
|
|
279
|
-
* @example
|
|
280
|
-
* ```ts
|
|
281
|
-
* const response = await fetch("https://example.com/large.csv", { signal: jupyter.signal });
|
|
282
|
-
* ```
|
|
283
|
-
* @example
|
|
284
|
-
* ```ts
|
|
285
|
-
* const signal = jupyter.signal;
|
|
286
|
-
* const ask = jupyter.comm.open("my.app", { question: "ready?" });
|
|
287
|
-
* const answer = await new Promise((resolve, reject) => {
|
|
288
|
-
* ask.onMsg((m) => resolve(m.data));
|
|
289
|
-
* signal.addEventListener("abort", () => { ask.close(); reject(signal.reason); }, { once: true });
|
|
290
|
-
* });
|
|
291
|
-
* ```
|
|
292
|
-
* @see jupyter-api.md#stopping-on-interrupt-jupyter-signal
|
|
293
|
-
*/
|
|
294
|
-
readonly signal: AbortSignal;
|
|
295
420
|
/**
|
|
296
421
|
* The parameters of this run: a frozen object, empty unless whoever
|
|
297
422
|
* started the kernel passed some, such as `ganymede execute -p
|
package/runtime/display.mjs
CHANGED
|
@@ -4,19 +4,27 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Ported from Deno's cli/js/40_jupyter.js (v2.9.6), which is
|
|
6
6
|
// Copyright 2018-2026 the Deno authors, MIT license. The formatting rules
|
|
7
|
-
// (display symbol, canvas, Vega,
|
|
8
|
-
//
|
|
7
|
+
// (display symbol, canvas, Vega, JPEG/PNG bytes, HTML/SVG elements) are
|
|
8
|
+
// unchanged; Polars-style DataFrames show through table.mjs instead (#475),
|
|
9
|
+
// as do Arrow tables and DuckDB results. The Deno internals it used are
|
|
10
|
+
// injected:
|
|
9
11
|
// Deno[Deno.internal].inspectArgs -> host.inspect
|
|
10
12
|
// op_base64_encode_from_buffer -> host.base64
|
|
11
13
|
// Deno.readFileSync -> host.readFile
|
|
12
14
|
// op_jupyter_broadcast -> host.send
|
|
13
15
|
// WebGPU texture/buffer display depends on Deno's wgpu bindings and is
|
|
14
|
-
// omitted. Comms and
|
|
15
|
-
//
|
|
16
|
+
// omitted. Comms, widgets, tables, SQL and charts (jupyter.comm,
|
|
17
|
+
// jupyter.widget, jupyter.table, jupyter.sql, jupyter.chart) are additions
|
|
18
|
+
// Deno doesn't have; see comm.mjs, widget.mjs, table.mjs, sql.mjs and
|
|
19
|
+
// chart.mjs.
|
|
16
20
|
|
|
17
21
|
import { createComms } from "./comm.mjs";
|
|
18
22
|
import { createHost } from "./host.mjs";
|
|
19
23
|
import { createWidgetFactory } from "./widget.mjs";
|
|
24
|
+
import { createSql } from "./sql.mjs";
|
|
25
|
+
import { createChart } from "./chart.mjs";
|
|
26
|
+
import { isTableLike, pageTable, tableBundle, tableSource } from "./table.mjs";
|
|
27
|
+
import { createTablePager } from "./table-paging.mjs";
|
|
20
28
|
|
|
21
29
|
export const $display = Symbol.for("Jupyter.display");
|
|
22
30
|
|
|
@@ -243,66 +251,6 @@ function summarizeChart(spec) {
|
|
|
243
251
|
}
|
|
244
252
|
}
|
|
245
253
|
|
|
246
|
-
function isDataFrameLike(obj) {
|
|
247
|
-
return obj !== null && typeof obj === "object" &&
|
|
248
|
-
obj.schema !== undefined && typeof obj.schema === "object" &&
|
|
249
|
-
typeof obj.head === "function" && typeof obj.toRecords === "function";
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function mapPolarsTypeToJSONSchema(colType) {
|
|
253
|
-
const typeMapping = {
|
|
254
|
-
Null: "null",
|
|
255
|
-
Bool: "boolean",
|
|
256
|
-
Int8: "integer",
|
|
257
|
-
Int16: "integer",
|
|
258
|
-
Int32: "integer",
|
|
259
|
-
Int64: "integer",
|
|
260
|
-
UInt8: "integer",
|
|
261
|
-
UInt16: "integer",
|
|
262
|
-
UInt32: "integer",
|
|
263
|
-
UInt64: "integer",
|
|
264
|
-
Float32: "number",
|
|
265
|
-
Float64: "number",
|
|
266
|
-
Date: "string",
|
|
267
|
-
Datetime: "string",
|
|
268
|
-
Utf8: "string",
|
|
269
|
-
Categorical: "string",
|
|
270
|
-
List: "array",
|
|
271
|
-
Struct: "object",
|
|
272
|
-
};
|
|
273
|
-
// Polars DataTypes only expose their name through toJSON().
|
|
274
|
-
const dataType = typeof colType?.toJSON === "function" ? colType.toJSON()["DataType"] : undefined;
|
|
275
|
-
return typeMapping[dataType] || "string";
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function extractDataFrame(df) {
|
|
279
|
-
const fields = [];
|
|
280
|
-
for (const [name, colType] of Object.entries(df.schema)) {
|
|
281
|
-
fields.push({ name, type: mapPolarsTypeToJSONSchema(colType) });
|
|
282
|
-
}
|
|
283
|
-
const data = df.head(50).toRecords();
|
|
284
|
-
let html = "<table><thead><tr>";
|
|
285
|
-
for (const field of fields) html += `<th>${escapeHTML(String(field.name))}</th>`;
|
|
286
|
-
html += "</tr></thead><tbody>";
|
|
287
|
-
for (const row of df.head(10).toRecords()) {
|
|
288
|
-
html += "<tr>";
|
|
289
|
-
for (const field of fields) html += `<td>${escapeHTML(String(row[field.name]))}</td>`;
|
|
290
|
-
html += "</tr>";
|
|
291
|
-
}
|
|
292
|
-
html += "</tbody></table>";
|
|
293
|
-
// Polars has `height`; the row count is left out when there isn't one.
|
|
294
|
-
const size = [];
|
|
295
|
-
if (Number.isInteger(df.height)) size.push(`${df.height} ${df.height === 1 ? "row" : "rows"}`);
|
|
296
|
-
size.push(`${fields.length} ${fields.length === 1 ? "column" : "columns"}`);
|
|
297
|
-
const columns = fields.slice(0, 20).map((field) => `${clip(String(field.name), 40)} (${field.type})`);
|
|
298
|
-
if (fields.length > 20) columns.push(`… ${fields.length - 20} more`);
|
|
299
|
-
return {
|
|
300
|
-
"application/vnd.dataresource+json": { data, schema: { fields } },
|
|
301
|
-
"text/html": html,
|
|
302
|
-
"text/plain": `DataFrame (${size.join(", ")})${columns.length ? `: ${columns.join(", ")}` : ""}`,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
254
|
function isCanvasLike(obj) {
|
|
307
255
|
return obj !== null && typeof obj === "object" && "toDataURL" in obj;
|
|
308
256
|
}
|
|
@@ -377,7 +325,8 @@ export function createJupyter(host) {
|
|
|
377
325
|
const vega = extractVega(obj);
|
|
378
326
|
if (vega) return vega;
|
|
379
327
|
}
|
|
380
|
-
|
|
328
|
+
// Arrow tables, Polars DataFrames and DuckDB results (#475).
|
|
329
|
+
if (isTableLike(obj)) return tableBundle(tableSource(obj));
|
|
381
330
|
if (isJpg(obj)) return { "image/jpeg": host.base64(obj) };
|
|
382
331
|
if (isPng(obj)) return { "image/png": host.base64(obj) };
|
|
383
332
|
if (isSVGElementLike(obj)) return { "image/svg+xml": obj.outerHTML };
|
|
@@ -409,14 +358,25 @@ export function createJupyter(host) {
|
|
|
409
358
|
);
|
|
410
359
|
}
|
|
411
360
|
|
|
361
|
+
// jupyter.table (#475): rows and columns as Ganymede's table output,
|
|
362
|
+
// built now so a bad value fails the call, not the display.
|
|
363
|
+
function table(value, options = {}) {
|
|
364
|
+
const bundle = tableBundle(tableSource(value, options), options);
|
|
365
|
+
return { [$display]: () => bundle };
|
|
366
|
+
}
|
|
367
|
+
|
|
412
368
|
function broadcast(msgType, content, { metadata = {}, buffers = [] } = {}) {
|
|
413
369
|
host.send(msgType, content, metadata, buffers);
|
|
414
370
|
}
|
|
415
371
|
|
|
372
|
+
// Set below, once the comms are: a table cut by the cap is kept for
|
|
373
|
+
// paging when it is sent as an output, not when it is formatted.
|
|
374
|
+
let pager;
|
|
375
|
+
|
|
416
376
|
async function display(obj, options = { raw: false, update: false }) {
|
|
417
377
|
const bundle = options.raw && isMediaBundle(obj) ? obj : await format(obj);
|
|
418
378
|
broadcast(options.update ? "update_display_data" : "display_data", {
|
|
419
|
-
data: bundle,
|
|
379
|
+
data: pageTable(bundle, pager, options.display_id),
|
|
420
380
|
metadata: {},
|
|
421
381
|
transient: options.display_id ? { display_id: options.display_id } : {},
|
|
422
382
|
});
|
|
@@ -427,7 +387,7 @@ export function createJupyter(host) {
|
|
|
427
387
|
try {
|
|
428
388
|
broadcast("execute_result", {
|
|
429
389
|
execution_count: executionCount,
|
|
430
|
-
data: await format(result),
|
|
390
|
+
data: pageTable(await format(result), pager),
|
|
431
391
|
metadata: {},
|
|
432
392
|
// nbclient crashes on a null `transient`; Deno learned this too.
|
|
433
393
|
transient: {},
|
|
@@ -492,7 +452,24 @@ export function createJupyter(host) {
|
|
|
492
452
|
});
|
|
493
453
|
}
|
|
494
454
|
|
|
455
|
+
// The cell's own import, so npm: resolution and the browser target's CDN
|
|
456
|
+
// apply to the packages jupyter.sql and jupyter.chart load.
|
|
457
|
+
const cellImport = (what) => (specifier) => {
|
|
458
|
+
const load = globalThis.__cdpk?.import;
|
|
459
|
+
if (typeof load !== "function") return Promise.reject(new Error(`${what}: no module loader in this runtime`));
|
|
460
|
+
return load(specifier);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// jupyter.sql (#477): DuckDB, loaded on the first query; an interrupt
|
|
464
|
+
// (jupyter.signal) interrupts the query.
|
|
465
|
+
const sql = createSql({ importModule: cellImport("jupyter.sql"), signal: () => current.signal });
|
|
466
|
+
|
|
467
|
+
// jupyter.chart (#479): vega, vega-lite or linkedom, loaded on first use.
|
|
468
|
+
const chart = createChart({ importModule: cellImport("jupyter.chart"), summarizeSpec: summarizeChart });
|
|
469
|
+
|
|
495
470
|
const comms = createComms((msgType, content, metadata, buffers) => host.send(msgType, content, metadata, buffers));
|
|
471
|
+
// Tables cut by the cap page through the kernel over a comm (ganymede#747).
|
|
472
|
+
pager = createTablePager(comms.api);
|
|
496
473
|
|
|
497
474
|
// jupyter.signal (#418): aborted when the kernel is interrupted, so cell
|
|
498
475
|
// code can stop waiting on what the interrupt can't reach, such as a comm
|
|
@@ -546,6 +523,9 @@ export function createJupyter(host) {
|
|
|
546
523
|
return readParams();
|
|
547
524
|
},
|
|
548
525
|
result,
|
|
526
|
+
table,
|
|
527
|
+
sql,
|
|
528
|
+
chart,
|
|
549
529
|
format,
|
|
550
530
|
md: taggedTemplate("text/markdown"),
|
|
551
531
|
html: taggedTemplate("text/html"),
|
package/runtime/host.d.mts
CHANGED
|
@@ -103,7 +103,7 @@ export interface Host {
|
|
|
103
103
|
call(method: string, params?: HostParams, options?: HostCallOptions): Promise<any>;
|
|
104
104
|
/** Other notebooks under the server's root, as far as this notebook's grant reaches. */
|
|
105
105
|
readonly notebooks: HostNotebooks;
|
|
106
|
-
/** This notebook's own cells.
|
|
106
|
+
/** This notebook's own cells. Ganymede 0.8.0 or later; an older server rejects `unavailable`. */
|
|
107
107
|
readonly self: HostSelf;
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -160,7 +160,8 @@ export interface HostNotebooks {
|
|
|
160
160
|
options?: HostCallOptions,
|
|
161
161
|
): Promise<NotebookSearch>;
|
|
162
162
|
/**
|
|
163
|
-
* A notebook's named results.
|
|
163
|
+
* A notebook's named results. Ganymede 0.8.0 or later; an older server
|
|
164
|
+
* rejects `unavailable`.
|
|
164
165
|
*
|
|
165
166
|
* @param params `path`: the notebook.
|
|
166
167
|
* @param options `signal` cancels the call.
|
|
@@ -172,8 +173,9 @@ export interface HostNotebooks {
|
|
|
172
173
|
*/
|
|
173
174
|
results(params: { path: string }, options?: HostCallOptions): Promise<any>;
|
|
174
175
|
/**
|
|
175
|
-
* Run a notebook with parameters and return its results
|
|
176
|
-
*
|
|
176
|
+
* Run a notebook with parameters and return its results, in a fresh
|
|
177
|
+
* kernel, when this notebook's grant names it. Ganymede 0.8.0 or later;
|
|
178
|
+
* an older server rejects `unavailable`.
|
|
177
179
|
*
|
|
178
180
|
* @param params `path`: the notebook; `params`: its parameters.
|
|
179
181
|
* @param options `signal` cancels the run.
|
|
@@ -186,7 +188,7 @@ export interface HostNotebooks {
|
|
|
186
188
|
run(params: { path: string; params?: HostParams }, options?: HostCallOptions): Promise<any>;
|
|
187
189
|
}
|
|
188
190
|
|
|
189
|
-
/** `host.self`: this notebook's own cells.
|
|
191
|
+
/** `host.self`: this notebook's own cells. Ganymede 0.8.0 or later; an older server rejects each with `unavailable`. */
|
|
190
192
|
export interface HostSelf {
|
|
191
193
|
/**
|
|
192
194
|
* Add a cell to this notebook.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Displayable } from "./display.d.mts";
|
|
2
|
+
|
|
3
|
+
export const NODE_API: string;
|
|
4
|
+
export const DUCKDB_WASM: string;
|
|
5
|
+
|
|
6
|
+
/** One column of a query's result. */
|
|
7
|
+
export interface SqlColumn {
|
|
8
|
+
name: string;
|
|
9
|
+
/** DuckDB's type name (`BIGINT`, `VARCHAR`, `DATE`), or Arrow's in the browser. */
|
|
10
|
+
type: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A query's result. It shows as a table (the same output as
|
|
15
|
+
* `jupyter.table`) when it is a cell's value or passed to
|
|
16
|
+
* `jupyter.display`.
|
|
17
|
+
*/
|
|
18
|
+
export interface SqlResult extends Displayable {
|
|
19
|
+
/** The result's columns, in order. */
|
|
20
|
+
readonly columns: SqlColumn[];
|
|
21
|
+
/**
|
|
22
|
+
* The rows as plain objects keyed by column name. Integers that fit a
|
|
23
|
+
* JavaScript number are numbers, larger ones `bigint`; `DATE` and
|
|
24
|
+
* `TIMESTAMP` values are `Date`s.
|
|
25
|
+
*/
|
|
26
|
+
readonly rows: Record<string, unknown>[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const SqlResult: new (columns: SqlColumn[], rows: Record<string, unknown>[]) => SqlResult & {
|
|
30
|
+
toTableSource(names?: string[]): import("./table.d.mts").TableSource;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Text that `jupyter.sql` puts into the query as written: see `sql.raw`. */
|
|
34
|
+
export interface SqlFragment {
|
|
35
|
+
readonly __sqlRaw: never;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `jupyter.sql`: a tagged template that runs SQL on the kernel's DuckDB.
|
|
40
|
+
* Each `${value}` becomes a bound parameter.
|
|
41
|
+
*/
|
|
42
|
+
export interface Sql {
|
|
43
|
+
/**
|
|
44
|
+
* Run the query, with each `${value}` as a bound parameter.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* await jupyter.sql`select ${"Ganymede"} as moon`;
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
(strings: TemplateStringsArray, ...values: unknown[]): Promise<SqlResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Run SQL from a string, with values for its `?` parameters. For a
|
|
54
|
+
* query built at run time; prefer the tag otherwise.
|
|
55
|
+
*
|
|
56
|
+
* @param text The SQL.
|
|
57
|
+
* @param params Values for its `?` placeholders, in order.
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const op = ">";
|
|
61
|
+
* await jupyter.sql.query(`select 2 ${op} ? as answer`, [1]);
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
query(text: string, params?: unknown[]): Promise<SqlResult>;
|
|
65
|
+
/**
|
|
66
|
+
* Text the tag puts into the query as written, rather than as a
|
|
67
|
+
* parameter: a table name or a file path built in code. Never pass it
|
|
68
|
+
* text from outside the notebook.
|
|
69
|
+
*
|
|
70
|
+
* @param text SQL text.
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const table = "moons";
|
|
74
|
+
* await jupyter.sql`select count(*) as n from ${jupyter.sql.raw(table)}`;
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
raw(text: string): SqlFragment;
|
|
78
|
+
/**
|
|
79
|
+
* Create (or replace) table `name` from a file DuckDB can read (CSV,
|
|
80
|
+
* Parquet or JSON, by path or URL), from an array of row objects, or
|
|
81
|
+
* from an Arrow table or Polars DataFrame. Rows go in as one JSON
|
|
82
|
+
* parameter, typed from the values: integers as `BIGINT`, numbers as
|
|
83
|
+
* `DOUBLE`, `Date`s as `TIMESTAMP`, arrays and objects as `JSON`.
|
|
84
|
+
*
|
|
85
|
+
* @param name The table's name.
|
|
86
|
+
* @param source A path or URL, rows, an Arrow table or a DataFrame.
|
|
87
|
+
* @throws {TypeError} `name` is empty, or the rows have no columns.
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* await jupyter.sql.load("moons", [
|
|
91
|
+
* { moon: "Io", radius_km: 1821.6 },
|
|
92
|
+
* { moon: "Ganymede", radius_km: 2634.1 },
|
|
93
|
+
* ]);
|
|
94
|
+
* await jupyter.sql`select moon from moons order by radius_km desc`;
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
load(name: string, source: unknown): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Close the database and free its memory. The next query opens a new,
|
|
100
|
+
* empty one.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* await jupyter.sql.close();
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
close(): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* The DuckDB connection itself, for what the tag doesn't cover: a
|
|
110
|
+
* `@duckdb/node-api` `DuckDBConnection` on Node, Deno and Bun, a
|
|
111
|
+
* duckdb-wasm `AsyncDuckDBConnection` in the browser.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* const conn = await jupyter.sql.connection();
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
connection(): Promise<unknown>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function decimalValue(words: ArrayLike<number> | number | bigint, scale: number): number;
|
|
122
|
+
export function toParam(value: unknown): unknown;
|
|
123
|
+
/** A parameter as a SQL literal. */
|
|
124
|
+
export function sqlLiteral(value: unknown): string;
|
|
125
|
+
/** The query with each `?` placeholder (outside strings, quoted names and comments) replaced by its parameter as a literal. */
|
|
126
|
+
export function inlineParams(text: string, params: unknown[]): string;
|
|
127
|
+
/** A DuckDB TIME as `HH:MM:SS[.fraction]`, from its count in the declared type's unit (microseconds by default). */
|
|
128
|
+
export function timeText(value: number | bigint, declared?: string): string;
|
|
129
|
+
/** Column names made unique: a repeated `id` becomes `id_1`, `id_2`, and so on. */
|
|
130
|
+
export function uniqueNames(names: readonly string[]): string[];
|
|
131
|
+
export function compose(strings: readonly string[], values: unknown[]): { text: string; params: unknown[] };
|
|
132
|
+
export function quoteIdent(name: string): string;
|
|
133
|
+
export function quoteString(text: string): string;
|
|
134
|
+
export function rowsAsJson(source: unknown): { structure: string | null; json: string };
|
|
135
|
+
|
|
136
|
+
export interface SqlEngine {
|
|
137
|
+
query: (text: string, params: unknown[]) => Promise<SqlResult>;
|
|
138
|
+
interrupt?: () => void;
|
|
139
|
+
close: () => Promise<void>;
|
|
140
|
+
connection: unknown;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createSql(options: {
|
|
144
|
+
importModule: (specifier: string) => Promise<any>;
|
|
145
|
+
signal?: () => AbortSignal | undefined;
|
|
146
|
+
openEngine?: () => Promise<SqlEngine>;
|
|
147
|
+
}): Sql;
|