@gradio/client 2.5.0 → 2.6.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/CHANGELOG.md +17 -0
- package/dist/browser.js +557 -438
- package/dist/helpers/init_helpers.d.ts +1 -1
- package/dist/helpers/init_helpers.d.ts.map +1 -1
- package/dist/index.cjs +177 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +170 -8
- package/dist/index.min.js +440 -321
- package/dist/types.d.ts +5 -5
- package/dist/upload.d.ts.map +1 -1
- package/dist/utils/bucket_sync.d.ts +38 -0
- package/dist/utils/bucket_sync.d.ts.map +1 -0
- package/dist/utils/run_history.d.ts +35 -0
- package/dist/utils/run_history.d.ts.map +1 -1
- package/dist/utils/submit.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/helpers/init_helpers.ts +21 -4
- package/src/index.ts +14 -0
- package/src/test/bucket_sync.test.ts +138 -0
- package/src/test/run_history.test.ts +113 -0
- package/src/test/submit.test.ts +38 -1
- package/src/test/upload.test.ts +25 -0
- package/src/types.ts +5 -5
- package/src/upload.ts +5 -1
- package/src/utils/bucket_sync.ts +153 -0
- package/src/utils/run_history.ts +133 -1
- package/src/utils/submit.ts +11 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the durable run history in an HF Hub bucket
|
|
3
|
+
* (`/gradio_api/run-history/*`).
|
|
4
|
+
*
|
|
5
|
+
* Read-only by design. Records are written by the server from the run it
|
|
6
|
+
* actually executed — there is no push function here, and nothing in this
|
|
7
|
+
* module can create a record.
|
|
8
|
+
*
|
|
9
|
+
* Every call names the bucket it is talking about. The server holds no
|
|
10
|
+
* per-session binding, so two tabs (or two apps on one origin) cannot end up
|
|
11
|
+
* reading each other's history.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const BUCKET_ID_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-][a-zA-Z0-9_./-]*$/;
|
|
15
|
+
|
|
16
|
+
export function is_valid_bucket_id(id: string): boolean {
|
|
17
|
+
if (!BUCKET_ID_RE.test(id)) return false;
|
|
18
|
+
return !id
|
|
19
|
+
.split("/")
|
|
20
|
+
.some((seg) => seg === "" || seg === "." || seg === "..");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Mirrors `HistoryRecord` in `gradio/history.py`. */
|
|
24
|
+
export interface HistoryRecord {
|
|
25
|
+
record_id: string;
|
|
26
|
+
endpoint: string;
|
|
27
|
+
inputs: unknown;
|
|
28
|
+
outputs: unknown;
|
|
29
|
+
started_at: string;
|
|
30
|
+
schema_version: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface HistoryResult<T> {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
status: number;
|
|
36
|
+
data: T;
|
|
37
|
+
detail?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function url(
|
|
41
|
+
root: string,
|
|
42
|
+
path: string,
|
|
43
|
+
params?: Record<string, string>
|
|
44
|
+
): string {
|
|
45
|
+
const query = params ? `?${new URLSearchParams(params)}` : "";
|
|
46
|
+
const base = root.replace(/\/+$/, "");
|
|
47
|
+
if (base) return `${base}/gradio_api/run-history/${path}${query}`;
|
|
48
|
+
// No configured root. A leading slash would drop any mount subpath
|
|
49
|
+
// (`mount_gradio_app(app, path="/myapp")`), so resolve against the document
|
|
50
|
+
// base, which gradio sets via <base href> on the app shell.
|
|
51
|
+
if (typeof document !== "undefined" && document.baseURI) {
|
|
52
|
+
return new URL(
|
|
53
|
+
`gradio_api/run-history/${path}${query}`,
|
|
54
|
+
document.baseURI
|
|
55
|
+
).toString();
|
|
56
|
+
}
|
|
57
|
+
return `gradio_api/run-history/${path}${query}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function parse_error(res: Response): Promise<string> {
|
|
61
|
+
try {
|
|
62
|
+
const j = await res.json();
|
|
63
|
+
return typeof j?.detail === "string" ? j.detail : `${res.status}`;
|
|
64
|
+
} catch {
|
|
65
|
+
return `${res.status}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function request<T>(
|
|
70
|
+
input: string,
|
|
71
|
+
init: RequestInit,
|
|
72
|
+
fallback: T,
|
|
73
|
+
pick: (body: any) => T
|
|
74
|
+
): Promise<HistoryResult<T>> {
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(input, { credentials: "include", ...init });
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
// An empty list and a failed request are different things: a 401 here
|
|
79
|
+
// means "sign in", not "no runs yet".
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
status: res.status,
|
|
83
|
+
data: fallback,
|
|
84
|
+
detail: await parse_error(res)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, status: res.status, data: pick(await res.json()) };
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return { ok: false, status: 0, data: fallback, detail: String(e) };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create the bucket if it does not exist and confirm it is writable. Stores
|
|
95
|
+
* nothing server-side — the caller keeps its own choice and names it on every
|
|
96
|
+
* later request.
|
|
97
|
+
*/
|
|
98
|
+
export async function connect_bucket(
|
|
99
|
+
root: string,
|
|
100
|
+
bucket_id: string
|
|
101
|
+
): Promise<HistoryResult<null>> {
|
|
102
|
+
if (!is_valid_bucket_id(bucket_id)) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 422,
|
|
106
|
+
data: null,
|
|
107
|
+
detail: "invalid bucket id"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return request(
|
|
111
|
+
url(root, "connect"),
|
|
112
|
+
{
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { "Content-Type": "application/json" },
|
|
115
|
+
body: JSON.stringify({ bucket_id })
|
|
116
|
+
},
|
|
117
|
+
null,
|
|
118
|
+
() => null
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function list_user_buckets(
|
|
123
|
+
root: string
|
|
124
|
+
): Promise<HistoryResult<string[]>> {
|
|
125
|
+
return request<string[]>(url(root, "buckets"), {}, [], (b) =>
|
|
126
|
+
Array.isArray(b?.buckets) ? b.buckets : []
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function list_bucket_records(
|
|
131
|
+
root: string,
|
|
132
|
+
bucket: string
|
|
133
|
+
): Promise<HistoryResult<HistoryRecord[]>> {
|
|
134
|
+
return request(url(root, "records", { bucket }), {}, [], (b) =>
|
|
135
|
+
Array.isArray(b?.records) ? b.records : []
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function asset_url(
|
|
140
|
+
root: string,
|
|
141
|
+
bucket: string,
|
|
142
|
+
endpoint: string,
|
|
143
|
+
record_id: string,
|
|
144
|
+
filename: string
|
|
145
|
+
): string {
|
|
146
|
+
return url(
|
|
147
|
+
root,
|
|
148
|
+
`records/${encodeURIComponent(endpoint)}/${encodeURIComponent(
|
|
149
|
+
record_id
|
|
150
|
+
)}/assets/${encodeURIComponent(filename)}`,
|
|
151
|
+
{ bucket }
|
|
152
|
+
);
|
|
153
|
+
}
|
package/src/utils/run_history.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { GradioEvent, StatusMessage } from "../types";
|
|
|
11
11
|
const KEY_ROOT = "gradio:run-history:";
|
|
12
12
|
const STORAGE_PREFIX = `${KEY_ROOT}v2:`;
|
|
13
13
|
const REPLAY_PREFIX = `${KEY_ROOT}replay:v2:`;
|
|
14
|
+
const DESTINATION_PREFIX = `${KEY_ROOT}destination:v1:`;
|
|
14
15
|
const MAX_RUNS = 100;
|
|
15
16
|
const MAX_APPS = 8;
|
|
16
17
|
|
|
@@ -26,6 +27,10 @@ export interface RunHistoryScope {
|
|
|
26
27
|
username?: string | null;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export type RunHistoryStorage =
|
|
31
|
+
| { type: "browser"; bucket_id?: string }
|
|
32
|
+
| { type: "bucket"; bucket_id: string };
|
|
33
|
+
|
|
29
34
|
/**
|
|
30
35
|
* Run history is a side effect of submitting, never the point of it, so no
|
|
31
36
|
* failure in here may propagate into the caller and break the app. Every
|
|
@@ -115,6 +120,45 @@ function replay_key(scope: RunHistoryScope | null | undefined): string | null {
|
|
|
115
120
|
return key ? key.replace(STORAGE_PREFIX, REPLAY_PREFIX) : null;
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
function destination_key(
|
|
124
|
+
scope: RunHistoryScope | null | undefined
|
|
125
|
+
): string | null {
|
|
126
|
+
const key = storage_key(scope);
|
|
127
|
+
return key ? key.replace(STORAGE_PREFIX, DESTINATION_PREFIX) : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function read_run_history_storage_impl(
|
|
131
|
+
scope: RunHistoryScope | null | undefined
|
|
132
|
+
): RunHistoryStorage {
|
|
133
|
+
const key = destination_key(scope);
|
|
134
|
+
if (!key) return { type: "browser" };
|
|
135
|
+
try {
|
|
136
|
+
const value = JSON.parse(window.localStorage.getItem(key) || "null");
|
|
137
|
+
if (typeof value?.bucket_id === "string") {
|
|
138
|
+
return value.type === "bucket"
|
|
139
|
+
? { type: "bucket", bucket_id: value.bucket_id }
|
|
140
|
+
: { type: "browser", bucket_id: value.bucket_id };
|
|
141
|
+
}
|
|
142
|
+
return { type: "browser" };
|
|
143
|
+
} catch {
|
|
144
|
+
return { type: "browser" };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function set_run_history_storage_impl(
|
|
149
|
+
scope: RunHistoryScope | null | undefined,
|
|
150
|
+
storage: RunHistoryStorage
|
|
151
|
+
): void {
|
|
152
|
+
const key = destination_key(scope);
|
|
153
|
+
if (!key) return;
|
|
154
|
+
if (storage.type === "browser" && !storage.bucket_id) {
|
|
155
|
+
window.localStorage.removeItem(key);
|
|
156
|
+
} else {
|
|
157
|
+
window.localStorage.setItem(key, JSON.stringify(storage));
|
|
158
|
+
}
|
|
159
|
+
notify_run_history_change();
|
|
160
|
+
}
|
|
161
|
+
|
|
118
162
|
/** When a run was most recently saved under a key, for deciding what to drop. */
|
|
119
163
|
function last_saved_at(key: string): number {
|
|
120
164
|
try {
|
|
@@ -140,7 +184,10 @@ function prune_apps(current_key: string): void {
|
|
|
140
184
|
const stale = [
|
|
141
185
|
// Keys written by an older layout can never be read again.
|
|
142
186
|
...keys.filter(
|
|
143
|
-
(key) =>
|
|
187
|
+
(key) =>
|
|
188
|
+
!key.startsWith(STORAGE_PREFIX) &&
|
|
189
|
+
!key.startsWith(REPLAY_PREFIX) &&
|
|
190
|
+
!key.startsWith(DESTINATION_PREFIX)
|
|
144
191
|
),
|
|
145
192
|
...keys
|
|
146
193
|
.filter((key) => key.startsWith(STORAGE_PREFIX) && key !== current_key)
|
|
@@ -308,7 +355,64 @@ function consume_run_history_replay_impl(
|
|
|
308
355
|
}
|
|
309
356
|
}
|
|
310
357
|
|
|
358
|
+
/**
|
|
359
|
+
* The parts of an app config a replayed run writes back into. Kept structural
|
|
360
|
+
* so every entry point — the SPA and the SSR app each carry their own `Config`
|
|
361
|
+
* declaration — can hand its config straight over.
|
|
362
|
+
*/
|
|
363
|
+
export interface ReplayTarget {
|
|
364
|
+
components: { id: number; type: string; props: Record<string, any> }[];
|
|
365
|
+
dependencies: {
|
|
366
|
+
id: number;
|
|
367
|
+
api_name?: string | null;
|
|
368
|
+
inputs: number[];
|
|
369
|
+
outputs: number[];
|
|
370
|
+
}[];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function restore_run_impl(config: ReplayTarget, run: StoredRun): boolean {
|
|
374
|
+
const dependency = config.dependencies.find(
|
|
375
|
+
(item) =>
|
|
376
|
+
item.id === run.fn_index ||
|
|
377
|
+
(typeof item.api_name === "string" &&
|
|
378
|
+
`/${item.api_name.replace(/^\//, "")}` === run.api_name)
|
|
379
|
+
);
|
|
380
|
+
if (!dependency) return false;
|
|
381
|
+
|
|
382
|
+
const inputs = Array.isArray(run.inputs)
|
|
383
|
+
? run.inputs
|
|
384
|
+
: Object.values((run.inputs ?? {}) as Record<string, unknown>);
|
|
385
|
+
const outputs = Array.isArray(run.outputs)
|
|
386
|
+
? run.outputs
|
|
387
|
+
: run.outputs === null || run.outputs === undefined
|
|
388
|
+
? []
|
|
389
|
+
: [run.outputs];
|
|
390
|
+
|
|
391
|
+
const restore = (ids: number[], saved: unknown[]): void => {
|
|
392
|
+
for (const [index, id] of ids.entries()) {
|
|
393
|
+
const component = config.components.find((item) => item.id === id);
|
|
394
|
+
if (!component || index >= saved.length) continue;
|
|
395
|
+
// `gr.State` is held on the server and always saved as null, so
|
|
396
|
+
// writing it back would wipe out the component's real default.
|
|
397
|
+
if (component.type === "state") continue;
|
|
398
|
+
component.props.value = saved[index];
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
restore(dependency.inputs, inputs);
|
|
403
|
+
restore(dependency.outputs, outputs);
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function apply_run_history_replay_impl(
|
|
408
|
+
config: ReplayTarget & RunHistoryScope
|
|
409
|
+
): boolean {
|
|
410
|
+
const run = consume_run_history_replay_impl(config);
|
|
411
|
+
return run ? restore_run_impl(config, run) : false;
|
|
412
|
+
}
|
|
413
|
+
|
|
311
414
|
function start_run_history_impl(options: StartRunOptions): string | null {
|
|
415
|
+
if (read_run_history_storage_impl(options).type === "bucket") return null;
|
|
312
416
|
const key = storage_key(options);
|
|
313
417
|
if (!key) return null;
|
|
314
418
|
|
|
@@ -466,6 +570,21 @@ export function read_run_history(
|
|
|
466
570
|
return safely(() => read_run_history_impl(scope), []);
|
|
467
571
|
}
|
|
468
572
|
|
|
573
|
+
export function read_run_history_storage(
|
|
574
|
+
scope: RunHistoryScope | null | undefined
|
|
575
|
+
): RunHistoryStorage {
|
|
576
|
+
return safely(() => read_run_history_storage_impl(scope), {
|
|
577
|
+
type: "browser"
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function set_run_history_storage(
|
|
582
|
+
scope: RunHistoryScope | null | undefined,
|
|
583
|
+
storage: RunHistoryStorage
|
|
584
|
+
): void {
|
|
585
|
+
safely(() => set_run_history_storage_impl(scope, storage), undefined);
|
|
586
|
+
}
|
|
587
|
+
|
|
469
588
|
export function clear_run_history(
|
|
470
589
|
scope: RunHistoryScope | null | undefined
|
|
471
590
|
): void {
|
|
@@ -492,6 +611,19 @@ export function consume_run_history_replay(
|
|
|
492
611
|
return safely(() => consume_run_history_replay_impl(scope), null);
|
|
493
612
|
}
|
|
494
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Applies the run staged by the history page, if this page load is the one it
|
|
616
|
+
* was staged for. Every entry point that renders an app has to call this, or
|
|
617
|
+
* "Load run" silently does nothing on that entry point.
|
|
618
|
+
*
|
|
619
|
+
* @returns whether a staged run was found and applied.
|
|
620
|
+
*/
|
|
621
|
+
export function apply_run_history_replay(
|
|
622
|
+
config: ReplayTarget & RunHistoryScope
|
|
623
|
+
): boolean {
|
|
624
|
+
return safely(() => apply_run_history_replay_impl(config), false);
|
|
625
|
+
}
|
|
626
|
+
|
|
495
627
|
export function start_run_history(options: StartRunOptions): string | null {
|
|
496
628
|
return safely(() => start_run_history_impl(options), null);
|
|
497
629
|
}
|
package/src/utils/submit.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import { apply_diff_stream, close_stream } from "./stream";
|
|
31
31
|
import { Client } from "../client";
|
|
32
32
|
import {
|
|
33
|
+
read_run_history_storage,
|
|
33
34
|
start_run_history,
|
|
34
35
|
update_run_history,
|
|
35
36
|
update_run_inputs
|
|
@@ -63,7 +64,7 @@ export function submit(
|
|
|
63
64
|
api_prefix
|
|
64
65
|
} = this;
|
|
65
66
|
|
|
66
|
-
const
|
|
67
|
+
const base_headers = additional_headers || { "x-gradio-user": "api" };
|
|
67
68
|
|
|
68
69
|
const that = this;
|
|
69
70
|
|
|
@@ -114,6 +115,13 @@ export function submit(
|
|
|
114
115
|
const history_enabled =
|
|
115
116
|
config.run_history !== false && this.options.record_history !== false;
|
|
116
117
|
const history_scope = { app_id: config.app_id, username: config.username };
|
|
118
|
+
const history_storage = read_run_history_storage(history_scope);
|
|
119
|
+
const addt_headers = {
|
|
120
|
+
...base_headers,
|
|
121
|
+
...(history_enabled && history_storage.type === "bucket"
|
|
122
|
+
? { "x-gradio-history-bucket": history_storage.bucket_id }
|
|
123
|
+
: {})
|
|
124
|
+
};
|
|
117
125
|
const history_run_id =
|
|
118
126
|
!history_enabled || !is_documented_endpoint
|
|
119
127
|
? null
|
|
@@ -385,7 +393,8 @@ export function submit(
|
|
|
385
393
|
...payload,
|
|
386
394
|
session_hash,
|
|
387
395
|
event_id
|
|
388
|
-
}
|
|
396
|
+
},
|
|
397
|
+
addt_headers
|
|
389
398
|
);
|
|
390
399
|
if (status !== 200) {
|
|
391
400
|
fire_event({
|