@gradio/client 2.5.1 → 2.7.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 +21 -0
- package/dist/browser.js +442 -311
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/helpers/data.d.ts +9 -0
- package/dist/helpers/data.d.ts.map +1 -1
- package/dist/helpers/init_helpers.d.ts +1 -1
- package/dist/helpers/init_helpers.d.ts.map +1 -1
- package/dist/index.cjs +207 -19
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +201 -20
- package/dist/index.min.js +481 -350
- 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 +9 -0
- package/dist/utils/run_history.d.ts.map +1 -1
- package/dist/utils/submit.d.ts.map +1 -1
- package/dist/utils/view_api.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +19 -15
- package/src/helpers/data.ts +77 -0
- package/src/helpers/init_helpers.ts +29 -14
- package/src/index.ts +12 -0
- package/src/test/bucket_sync.test.ts +138 -0
- package/src/test/data.test.ts +95 -0
- package/src/test/init.test.ts +47 -1
- package/src/test/run_history.test.ts +46 -0
- package/src/test/server.ts +3 -0
- package/src/test/submit.test.ts +75 -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 +64 -1
- package/src/utils/submit.ts +21 -3
- package/src/utils/view_api.ts +4 -1
package/src/test/init.test.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
expect,
|
|
8
8
|
vi
|
|
9
9
|
} from "vitest";
|
|
10
|
-
|
|
11
10
|
import { Client, client, duplicate } from "..";
|
|
12
11
|
import {
|
|
13
12
|
transformed_api_info,
|
|
@@ -16,6 +15,7 @@ import {
|
|
|
16
15
|
} from "./test_data";
|
|
17
16
|
import { initialise_server } from "./server";
|
|
18
17
|
import { SPACE_NOT_FOUND_MSG } from "../constants";
|
|
18
|
+
import { HttpResponse, http } from "msw";
|
|
19
19
|
|
|
20
20
|
const app_reference = "hmb/hello_world";
|
|
21
21
|
const broken_app_reference = "hmb/bye_world";
|
|
@@ -65,6 +65,29 @@ describe("Client class", () => {
|
|
|
65
65
|
expect(app.config).toEqual(config_response);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
test("forwards a page query when resolving config and API info", async () => {
|
|
69
|
+
const requested_urls: string[] = [];
|
|
70
|
+
server.use(
|
|
71
|
+
http.get(`${direct_app_reference}/config`, ({ request }) => {
|
|
72
|
+
requested_urls.push(request.url);
|
|
73
|
+
return HttpResponse.json(config_response);
|
|
74
|
+
}),
|
|
75
|
+
http.get(`${direct_app_reference}/info`, ({ request }) => {
|
|
76
|
+
requested_urls.push(request.url);
|
|
77
|
+
return HttpResponse.json(response_api_info);
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await Client.connect(direct_app_reference, {
|
|
82
|
+
query_params: { page: "details" }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(requested_urls).toEqual([
|
|
86
|
+
`${direct_app_reference}/config?page=details`,
|
|
87
|
+
`${direct_app_reference}/info?page=details`
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
|
|
68
91
|
test("connecting successfully to a private running app with a space reference", async () => {
|
|
69
92
|
const app = await Client.connect("hmb/secret_world", {
|
|
70
93
|
token: "hf_123"
|
|
@@ -76,6 +99,29 @@ describe("Client class", () => {
|
|
|
76
99
|
});
|
|
77
100
|
});
|
|
78
101
|
|
|
102
|
+
test("signs initial file values in a private Space config", async () => {
|
|
103
|
+
const props = config_response.components[0].props;
|
|
104
|
+
props.value = {
|
|
105
|
+
path: "/tmp/cat.png",
|
|
106
|
+
url: `${secret_direct_app_reference}/gradio_api/file=/tmp/cat.png`,
|
|
107
|
+
meta: { _type: "gradio.FileData" }
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
const app = await Client.connect("hmb/secret_world", {
|
|
111
|
+
token: "hf_123"
|
|
112
|
+
});
|
|
113
|
+
const value = app.config?.components[0].props.value as {
|
|
114
|
+
url: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
expect(value.url).toBe(
|
|
118
|
+
`${secret_direct_app_reference}/gradio_api/file=/tmp/cat.png?__sign=jwt_123`
|
|
119
|
+
);
|
|
120
|
+
} finally {
|
|
121
|
+
delete props.value;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
79
125
|
test("connecting successfully to a private running app with a direct app URL ", async () => {
|
|
80
126
|
const app = await Client.connect(secret_direct_app_reference, {
|
|
81
127
|
token: "hf_123"
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
delete_run_history,
|
|
8
8
|
on_run_history_change,
|
|
9
9
|
read_run_history,
|
|
10
|
+
read_run_history_storage,
|
|
10
11
|
run_history_url,
|
|
12
|
+
set_run_history_storage,
|
|
11
13
|
stage_run_history_replay,
|
|
12
14
|
start_run_history,
|
|
13
15
|
update_run_history,
|
|
@@ -25,6 +27,8 @@ const replacement_apps = Array.from(
|
|
|
25
27
|
const in_browser = typeof window !== "undefined";
|
|
26
28
|
|
|
27
29
|
afterEach(() => {
|
|
30
|
+
set_run_history_storage(scope, { type: "browser" });
|
|
31
|
+
set_run_history_storage(other_scope, { type: "browser" });
|
|
28
32
|
clear_run_history(scope);
|
|
29
33
|
clear_run_history(other_scope);
|
|
30
34
|
for (const replacement_app of replacement_apps) {
|
|
@@ -34,6 +38,48 @@ afterEach(() => {
|
|
|
34
38
|
});
|
|
35
39
|
|
|
36
40
|
describe.skipIf(!in_browser)("run history", () => {
|
|
41
|
+
test("uses this browser as the default history storage", () => {
|
|
42
|
+
expect(read_run_history_storage(scope)).toEqual({ type: "browser" });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("remembers a bucket while switching storage destinations", () => {
|
|
46
|
+
set_run_history_storage(scope, {
|
|
47
|
+
type: "bucket",
|
|
48
|
+
bucket_id: "alice/app-history"
|
|
49
|
+
});
|
|
50
|
+
expect(read_run_history_storage(scope)).toEqual({
|
|
51
|
+
type: "bucket",
|
|
52
|
+
bucket_id: "alice/app-history"
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
set_run_history_storage(scope, {
|
|
56
|
+
type: "browser",
|
|
57
|
+
bucket_id: "alice/app-history"
|
|
58
|
+
});
|
|
59
|
+
expect(read_run_history_storage(scope)).toEqual({
|
|
60
|
+
type: "browser",
|
|
61
|
+
bucket_id: "alice/app-history"
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("does not duplicate bucket runs in local storage", () => {
|
|
66
|
+
set_run_history_storage(scope, {
|
|
67
|
+
type: "bucket",
|
|
68
|
+
bucket_id: "alice/app-history"
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const id = start_run_history({
|
|
72
|
+
app_id,
|
|
73
|
+
endpoint: "/predict",
|
|
74
|
+
api_name: "/predict",
|
|
75
|
+
fn_index: 0,
|
|
76
|
+
inputs: ["hello"]
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(id).toBeNull();
|
|
80
|
+
expect(read_run_history(scope)).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
37
83
|
test("stores runs per app id with the page and inputs", () => {
|
|
38
84
|
const id = start_run_history({
|
|
39
85
|
app_id,
|
package/src/test/server.ts
CHANGED
|
@@ -8,6 +8,7 @@ const IS_NODE =
|
|
|
8
8
|
interface MockServer {
|
|
9
9
|
start: (opts: StartOptions) => void | ReturnType<SetupWorker["start"]>;
|
|
10
10
|
stop: () => void | Promise<void>;
|
|
11
|
+
use: (...handlers: any[]) => void;
|
|
11
12
|
resetHandlers: (...handlers: any[]) => void;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -18,6 +19,7 @@ export async function initialise_server(): Promise<MockServer> {
|
|
|
18
19
|
return {
|
|
19
20
|
start: (opts: StartOptions) => server.listen(opts),
|
|
20
21
|
stop: () => server.close(),
|
|
22
|
+
use: (...h) => server.use(...h),
|
|
21
23
|
resetHandlers: (...h) => server.resetHandlers(...h)
|
|
22
24
|
};
|
|
23
25
|
}
|
|
@@ -26,6 +28,7 @@ export async function initialise_server(): Promise<MockServer> {
|
|
|
26
28
|
return {
|
|
27
29
|
start: (opts: StartOptions) => worker.start(opts),
|
|
28
30
|
stop: () => worker.stop(),
|
|
31
|
+
use: (...h) => worker.use(...h),
|
|
29
32
|
resetHandlers: (...h) => worker.resetHandlers(...h)
|
|
30
33
|
};
|
|
31
34
|
}
|
package/src/test/submit.test.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, beforeAll, afterEach, afterAll, test, expect } from "vitest";
|
|
2
|
+
import { HttpResponse, http } from "msw";
|
|
2
3
|
|
|
3
4
|
import { Client } from "../client";
|
|
5
|
+
import { set_run_history_storage } from "../utils/run_history";
|
|
6
|
+
import { direct_space_url } from "./handlers";
|
|
4
7
|
import { initialise_server } from "./server";
|
|
5
8
|
|
|
6
9
|
let server: Awaited<ReturnType<typeof initialise_server>>;
|
|
@@ -9,7 +12,12 @@ beforeAll(async () => {
|
|
|
9
12
|
server = await initialise_server();
|
|
10
13
|
await server.start({ quiet: true });
|
|
11
14
|
});
|
|
12
|
-
afterEach(() =>
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
server.resetHandlers();
|
|
17
|
+
if (typeof window !== "undefined") {
|
|
18
|
+
set_run_history_storage({ app_id: 123 }, { type: "browser" });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
13
21
|
afterAll(() => server.stop());
|
|
14
22
|
|
|
15
23
|
async function race_with_timeout<T>(
|
|
@@ -29,6 +37,72 @@ async function race_with_timeout<T>(
|
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
describe("submit iterator", () => {
|
|
40
|
+
test("signs private Space file URLs before publishing data events", async () => {
|
|
41
|
+
const app = await Client.connect("hmb/hello_world", {
|
|
42
|
+
token: "hf_123",
|
|
43
|
+
events: ["data", "status"]
|
|
44
|
+
});
|
|
45
|
+
app.stream_status.open = true;
|
|
46
|
+
|
|
47
|
+
const iterator = app.submit("/predict", ["hi"]);
|
|
48
|
+
const event_id = await iterator.wait_for_id();
|
|
49
|
+
const callback = app.event_callbacks[event_id as string];
|
|
50
|
+
|
|
51
|
+
const events: any[] = [];
|
|
52
|
+
const consumer = (async () => {
|
|
53
|
+
for await (const event of iterator) events.push(event);
|
|
54
|
+
})();
|
|
55
|
+
|
|
56
|
+
await callback({
|
|
57
|
+
msg: "process_completed",
|
|
58
|
+
output: {
|
|
59
|
+
data: [
|
|
60
|
+
{
|
|
61
|
+
path: "/tmp/cat.png",
|
|
62
|
+
url: `${direct_space_url}/gradio_api/file=/tmp/cat.png`,
|
|
63
|
+
meta: { _type: "gradio.FileData" }
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
success: true
|
|
68
|
+
});
|
|
69
|
+
await consumer;
|
|
70
|
+
|
|
71
|
+
const data_event = events.find((event) => event.type === "data");
|
|
72
|
+
expect(data_event.data[0].url).toBe(
|
|
73
|
+
`${direct_space_url}/gradio_api/file=/tmp/cat.png?__sign=jwt_123`
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test.skipIf(typeof window === "undefined")(
|
|
78
|
+
"sends the selected history bucket with queued submissions",
|
|
79
|
+
async () => {
|
|
80
|
+
const app = await Client.connect("hmb/hello_world");
|
|
81
|
+
const scope = {
|
|
82
|
+
app_id: app.config?.app_id,
|
|
83
|
+
username: app.config?.username
|
|
84
|
+
};
|
|
85
|
+
set_run_history_storage(scope, {
|
|
86
|
+
type: "bucket",
|
|
87
|
+
bucket_id: "alice/app-history"
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
let header: string | null = null;
|
|
91
|
+
server.resetHandlers(
|
|
92
|
+
http.post(`${direct_space_url}/queue/join`, ({ request }) => {
|
|
93
|
+
header = request.headers.get("x-gradio-history-bucket");
|
|
94
|
+
return HttpResponse.json({ event_id: "bucket-event" });
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const iterator = app.submit("/predict", ["hi"]);
|
|
99
|
+
await expect(iterator.wait_for_id()).resolves.toBe("bucket-event");
|
|
100
|
+
expect(header).toBe("alice/app-history");
|
|
101
|
+
await iterator.return();
|
|
102
|
+
set_run_history_storage(scope, { type: "browser" });
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
32
106
|
test("next() after the iterator is closed resolves to {done: true}", async () => {
|
|
33
107
|
const app = await Client.connect("hmb/hello_world");
|
|
34
108
|
// Avoid opening a real SSE stream — the test does not need one.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { Client } from "../client";
|
|
4
|
+
import { FileData, upload } from "../upload";
|
|
5
|
+
|
|
6
|
+
describe("upload", () => {
|
|
7
|
+
it("encodes uploaded file paths in file URLs", async () => {
|
|
8
|
+
const path = "/tmp/computer%20vision#Huggy.png";
|
|
9
|
+
const client = {
|
|
10
|
+
api_prefix: "/gradio_api",
|
|
11
|
+
upload_files: vi.fn().mockResolvedValue({ files: [path] })
|
|
12
|
+
} as unknown as Client;
|
|
13
|
+
const input = new FileData({
|
|
14
|
+
path,
|
|
15
|
+
blob: new File([], "computer%20vision#Huggy.png")
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const result = await upload.call(client, [input], "https://example.com");
|
|
19
|
+
|
|
20
|
+
expect(result?.[0]).toMatchObject({
|
|
21
|
+
path,
|
|
22
|
+
url: "https://example.com/gradio_api/file=/tmp/computer%2520vision%23Huggy.png"
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -354,11 +354,11 @@ export interface ClientOptions {
|
|
|
354
354
|
*/
|
|
355
355
|
oauth_token?: string;
|
|
356
356
|
/**
|
|
357
|
-
* Whether to save each call's inputs and outputs
|
|
358
|
-
*
|
|
359
|
-
* to true, and only ever applies in a browser: in Node there is no
|
|
360
|
-
*
|
|
361
|
-
* with `run_history=False` on `launch()`, which takes precedence
|
|
357
|
+
* Whether to save each call's inputs and outputs to the history destination
|
|
358
|
+
* selected in this browser (local storage by default, or a connected bucket).
|
|
359
|
+
* Defaults to true, and only ever applies in a browser: in Node there is no
|
|
360
|
+
* selected destination and nothing is recorded. The app can turn it off for
|
|
361
|
+
* everyone with `run_history=False` on `launch()`, which takes precedence.
|
|
362
362
|
*/
|
|
363
363
|
record_history?: boolean;
|
|
364
364
|
}
|
package/src/upload.ts
CHANGED
|
@@ -30,10 +30,14 @@ export async function upload(
|
|
|
30
30
|
} else {
|
|
31
31
|
if (response.files) {
|
|
32
32
|
return response.files.map((f, i) => {
|
|
33
|
+
const encoded_path = f
|
|
34
|
+
.split("/")
|
|
35
|
+
.map((segment) => encodeURIComponent(segment))
|
|
36
|
+
.join("/");
|
|
33
37
|
const file = new FileData({
|
|
34
38
|
...file_data[i],
|
|
35
39
|
path: f,
|
|
36
|
-
url: `${root_url}${this.api_prefix}/file=${
|
|
40
|
+
url: `${root_url}${this.api_prefix}/file=${encoded_path}`
|
|
37
41
|
});
|
|
38
42
|
return file;
|
|
39
43
|
});
|
|
@@ -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)
|
|
@@ -365,6 +412,7 @@ function apply_run_history_replay_impl(
|
|
|
365
412
|
}
|
|
366
413
|
|
|
367
414
|
function start_run_history_impl(options: StartRunOptions): string | null {
|
|
415
|
+
if (read_run_history_storage_impl(options).type === "bucket") return null;
|
|
368
416
|
const key = storage_key(options);
|
|
369
417
|
if (!key) return null;
|
|
370
418
|
|
|
@@ -522,6 +570,21 @@ export function read_run_history(
|
|
|
522
570
|
return safely(() => read_run_history_impl(scope), []);
|
|
523
571
|
}
|
|
524
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
|
+
|
|
525
588
|
export function clear_run_history(
|
|
526
589
|
scope: RunHistoryScope | null | undefined
|
|
527
590
|
): void {
|
package/src/utils/submit.ts
CHANGED
|
@@ -10,7 +10,12 @@ import type {
|
|
|
10
10
|
SubmitIterable
|
|
11
11
|
} from "../types";
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
skip_queue,
|
|
15
|
+
post_message,
|
|
16
|
+
handle_payload,
|
|
17
|
+
sign_file_urls
|
|
18
|
+
} from "../helpers/data";
|
|
14
19
|
import { get_zerogpu_origin } from "../helpers/zerogpu";
|
|
15
20
|
import {
|
|
16
21
|
handle_message,
|
|
@@ -30,6 +35,7 @@ import {
|
|
|
30
35
|
import { apply_diff_stream, close_stream } from "./stream";
|
|
31
36
|
import { Client } from "../client";
|
|
32
37
|
import {
|
|
38
|
+
read_run_history_storage,
|
|
33
39
|
start_run_history,
|
|
34
40
|
update_run_history,
|
|
35
41
|
update_run_inputs
|
|
@@ -63,12 +69,13 @@ export function submit(
|
|
|
63
69
|
api_prefix
|
|
64
70
|
} = this;
|
|
65
71
|
|
|
66
|
-
const
|
|
72
|
+
const base_headers = additional_headers || { "x-gradio-user": "api" };
|
|
67
73
|
|
|
68
74
|
const that = this;
|
|
69
75
|
|
|
70
76
|
if (!api_info) throw new Error(NO_API_INFO_MSG);
|
|
71
77
|
if (!config) throw new Error("Could not resolve app config");
|
|
78
|
+
const root = config.root;
|
|
72
79
|
|
|
73
80
|
let { fn_index, endpoint_info, dependency } = get_endpoint_info(
|
|
74
81
|
api_info,
|
|
@@ -114,6 +121,13 @@ export function submit(
|
|
|
114
121
|
const history_enabled =
|
|
115
122
|
config.run_history !== false && this.options.record_history !== false;
|
|
116
123
|
const history_scope = { app_id: config.app_id, username: config.username };
|
|
124
|
+
const history_storage = read_run_history_storage(history_scope);
|
|
125
|
+
const addt_headers = {
|
|
126
|
+
...base_headers,
|
|
127
|
+
...(history_enabled && history_storage.type === "bucket"
|
|
128
|
+
? { "x-gradio-history-bucket": history_storage.bucket_id }
|
|
129
|
+
: {})
|
|
130
|
+
};
|
|
117
131
|
const history_run_id =
|
|
118
132
|
!history_enabled || !is_documented_endpoint
|
|
119
133
|
? null
|
|
@@ -162,6 +176,9 @@ export function submit(
|
|
|
162
176
|
// event subscription methods
|
|
163
177
|
function fire_event(event: GradioEvent): void {
|
|
164
178
|
update_run_history(history_scope, history_run_id, event);
|
|
179
|
+
if (event.type === "data" || event.type === "render") {
|
|
180
|
+
sign_file_urls(event.data, root, api_prefix, that.jwt);
|
|
181
|
+
}
|
|
165
182
|
if (all_events || events_to_publish[event.type]) {
|
|
166
183
|
push_event(event);
|
|
167
184
|
}
|
|
@@ -385,7 +402,8 @@ export function submit(
|
|
|
385
402
|
...payload,
|
|
386
403
|
session_hash,
|
|
387
404
|
event_id
|
|
388
|
-
}
|
|
405
|
+
},
|
|
406
|
+
addt_headers
|
|
389
407
|
);
|
|
390
408
|
if (status !== 200) {
|
|
391
409
|
fire_event({
|
package/src/utils/view_api.ts
CHANGED
|
@@ -28,7 +28,10 @@ export async function view_api(this: Client): Promise<any> {
|
|
|
28
28
|
if (typeof window !== "undefined" && window.gradio_api_info) {
|
|
29
29
|
api_info = window.gradio_api_info;
|
|
30
30
|
} else {
|
|
31
|
-
const url =
|
|
31
|
+
const url = new URL(
|
|
32
|
+
join_urls(config.root, this.api_prefix, API_INFO_URL)
|
|
33
|
+
);
|
|
34
|
+
if (this.page !== null) url.searchParams.set("page", this.page);
|
|
32
35
|
response = await this.fetch(url, {
|
|
33
36
|
headers,
|
|
34
37
|
credentials: this.options.credentials ?? "same-origin"
|