@gradio/client 0.17.0 → 0.18.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/client.d.ts +15 -4
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/constants.d.ts +8 -2
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/helpers/data.d.ts +2 -2
  7. package/dist/helpers/data.d.ts.map +1 -1
  8. package/dist/helpers/init_helpers.d.ts.map +1 -1
  9. package/dist/helpers/spaces.d.ts.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +219 -176
  13. package/dist/test/handlers.d.ts +3 -0
  14. package/dist/test/handlers.d.ts.map +1 -0
  15. package/dist/test/mock_eventsource.d.ts +2 -0
  16. package/dist/test/mock_eventsource.d.ts.map +1 -0
  17. package/dist/test/server.d.ts +2 -0
  18. package/dist/test/server.d.ts.map +1 -0
  19. package/dist/test/test_data.d.ts +76 -0
  20. package/dist/test/test_data.d.ts.map +1 -0
  21. package/dist/types.d.ts +4 -3
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/upload.d.ts +2 -2
  24. package/dist/upload.d.ts.map +1 -1
  25. package/dist/utils/duplicate.d.ts.map +1 -1
  26. package/dist/utils/stream.d.ts +1 -1
  27. package/dist/utils/stream.d.ts.map +1 -1
  28. package/dist/utils/submit.d.ts.map +1 -1
  29. package/dist/utils/upload_files.d.ts.map +1 -1
  30. package/dist/utils/view_api.d.ts.map +1 -1
  31. package/package.json +8 -2
  32. package/src/client.ts +50 -24
  33. package/src/constants.ts +9 -2
  34. package/src/helpers/api_info.ts +4 -4
  35. package/src/helpers/data.ts +46 -28
  36. package/src/helpers/init_helpers.ts +7 -11
  37. package/src/helpers/spaces.ts +8 -3
  38. package/src/index.ts +1 -1
  39. package/src/test/api_info.test.ts +456 -0
  40. package/src/test/data.test.ts +281 -0
  41. package/src/test/handlers.ts +438 -0
  42. package/src/test/init.test.ts +139 -0
  43. package/src/test/init_helpers.test.ts +94 -0
  44. package/src/test/mock_eventsource.ts +11 -0
  45. package/src/test/post_data.test.ts +45 -0
  46. package/src/test/server.ts +6 -0
  47. package/src/test/spaces.test.ts +145 -0
  48. package/src/test/stream.test.ts +67 -0
  49. package/src/test/test_data.ts +557 -0
  50. package/src/test/upload_files.test.ts +42 -0
  51. package/src/test/view_api.test.ts +53 -0
  52. package/src/types.ts +4 -3
  53. package/src/upload.ts +4 -8
  54. package/src/utils/duplicate.ts +20 -3
  55. package/src/utils/handle_blob.ts +1 -1
  56. package/src/utils/post_data.ts +1 -1
  57. package/src/utils/stream.ts +29 -20
  58. package/src/utils/submit.ts +23 -15
  59. package/src/utils/upload_files.ts +11 -6
  60. package/src/utils/view_api.ts +4 -7
  61. package/vite.config.js +7 -0
  62. package/src/utils/client.node-test.ts +0 -173
@@ -5,6 +5,7 @@ import {
5
5
  } from "../helpers/spaces";
6
6
  import type { DuplicateOptions } from "../types";
7
7
  import { Client } from "../client";
8
+ import { SPACE_METADATA_ERROR_MSG } from "../constants";
8
9
 
9
10
  export async function duplicate(
10
11
  app_reference: string,
@@ -47,8 +48,12 @@ export async function duplicate(
47
48
 
48
49
  let original_hardware;
49
50
 
50
- if (!hardware) {
51
- original_hardware = await get_space_hardware(app_reference, hf_token);
51
+ try {
52
+ if (!hardware) {
53
+ original_hardware = await get_space_hardware(app_reference, hf_token);
54
+ }
55
+ } catch (e) {
56
+ throw Error(SPACE_METADATA_ERROR_MSG + (e as Error).message);
52
57
  }
53
58
 
54
59
  const requested_hardware = hardware || original_hardware || "cpu-basic";
@@ -80,8 +85,20 @@ export async function duplicate(
80
85
  const duplicated_space = await response.json();
81
86
 
82
87
  await set_space_timeout(`${user}/${space_name}`, timeout || 300, hf_token);
83
- return await Client.connect(duplicated_space.url, options);
88
+
89
+ return await Client.connect(
90
+ get_space_reference(duplicated_space.url),
91
+ options
92
+ );
84
93
  } catch (e: any) {
85
94
  throw new Error(e);
86
95
  }
87
96
  }
97
+
98
+ function get_space_reference(url: string): any {
99
+ const regex = /https:\/\/huggingface.co\/spaces\/([^/]+\/[^/]+)/;
100
+ const match = url.match(regex);
101
+ if (match) {
102
+ return match[1];
103
+ }
104
+ }
@@ -29,7 +29,7 @@ export async function handle_blob(
29
29
  path,
30
30
  file_url,
31
31
  type,
32
- name: blob?.name
32
+ name: blob instanceof File ? blob?.name : undefined
33
33
  };
34
34
  })
35
35
  );
@@ -16,7 +16,7 @@ export async function post_data(
16
16
  headers.Authorization = `Bearer ${this.options.hf_token}`;
17
17
  }
18
18
  try {
19
- var response = await this.fetch_implementation(url, {
19
+ var response = await this.fetch(url, {
20
20
  method: "POST",
21
21
  body: JSON.stringify(body),
22
22
  headers: { ...headers, ...additional_headers }
@@ -7,7 +7,8 @@ export function open_stream(this: Client): void {
7
7
  unclosed_events,
8
8
  pending_stream_messages,
9
9
  stream_status,
10
- config
10
+ config,
11
+ jwt
11
12
  } = this;
12
13
 
13
14
  if (!config) {
@@ -16,31 +17,35 @@ export function open_stream(this: Client): void {
16
17
 
17
18
  stream_status.open = true;
18
19
 
19
- let event_source: EventSource | null = null;
20
+ let stream: EventSource | null = null;
20
21
  let params = new URLSearchParams({
21
22
  session_hash: this.session_hash
22
23
  }).toString();
23
24
 
24
25
  let url = new URL(`${config.root}/queue/data?${params}`);
25
- event_source = this.eventSource_factory(url);
26
26
 
27
- if (!event_source) {
28
- throw new Error("Cannot connect to sse endpoint: " + url.toString());
27
+ if (jwt) {
28
+ url.searchParams.set("__sign", jwt);
29
29
  }
30
30
 
31
- event_source.onmessage = async function (event) {
31
+ stream = this.stream_factory(url);
32
+
33
+ if (!stream) {
34
+ console.warn("Cannot connect to SSE endpoint: " + url.toString());
35
+ return;
36
+ }
37
+
38
+ stream.onmessage = async function (event: MessageEvent) {
32
39
  let _data = JSON.parse(event.data);
33
40
  if (_data.msg === "close_stream") {
34
- close_stream(stream_status, event_source);
41
+ close_stream(stream_status, stream);
35
42
  return;
36
43
  }
37
44
  const event_id = _data.event_id;
38
45
  if (!event_id) {
39
46
  await Promise.all(
40
- Object.keys(event_callbacks).map(
41
- (event_id) =>
42
- // @ts-ignore
43
- event_callbacks[event_id](_data) // todo: check event_callbacks
47
+ Object.keys(event_callbacks).map((event_id) =>
48
+ event_callbacks[event_id](_data)
44
49
  )
45
50
  );
46
51
  } else if (event_callbacks[event_id] && config) {
@@ -50,11 +55,16 @@ export function open_stream(this: Client): void {
50
55
  ) {
51
56
  unclosed_events.delete(event_id);
52
57
  if (unclosed_events.size === 0) {
53
- close_stream(stream_status, event_source);
58
+ close_stream(stream_status, stream);
54
59
  }
55
60
  }
56
- let fn = event_callbacks[event_id];
57
- window.setTimeout(fn, 0, _data); // need to do this to put the event on the end of the event loop, so the browser can refresh between callbacks and not freeze in case of quick generations. See https://github.com/gradio-app/gradio/pull/7055
61
+ let fn: (data: any) => void = event_callbacks[event_id];
62
+
63
+ if (typeof window !== "undefined") {
64
+ window.setTimeout(fn, 0, _data); // need to do this to put the event on the end of the event loop, so the browser can refresh between callbacks and not freeze in case of quick generations. See https://github.com/gradio-app/gradio/pull/7055
65
+ } else {
66
+ setImmediate(fn, _data);
67
+ }
58
68
  } else {
59
69
  if (!pending_stream_messages[event_id]) {
60
70
  pending_stream_messages[event_id] = [];
@@ -62,27 +72,26 @@ export function open_stream(this: Client): void {
62
72
  pending_stream_messages[event_id].push(_data);
63
73
  }
64
74
  };
65
- event_source.onerror = async function () {
75
+ stream.onerror = async function () {
66
76
  await Promise.all(
67
77
  Object.keys(event_callbacks).map((event_id) =>
68
- // @ts-ignore
69
78
  event_callbacks[event_id]({
70
79
  msg: "unexpected_error",
71
80
  message: BROKEN_CONNECTION_MSG
72
81
  })
73
82
  )
74
83
  );
75
- close_stream(stream_status, event_source);
84
+ close_stream(stream_status, stream);
76
85
  };
77
86
  }
78
87
 
79
88
  export function close_stream(
80
89
  stream_status: { open: boolean },
81
- event_source: EventSource | null
90
+ stream: EventSource | null
82
91
  ): void {
83
- if (stream_status && event_source) {
92
+ if (stream_status && stream) {
84
93
  stream_status.open = false;
85
- event_source?.close();
94
+ stream?.close();
86
95
  }
87
96
  }
88
97
 
@@ -31,7 +31,7 @@ export function submit(
31
31
  try {
32
32
  const { hf_token } = this.options;
33
33
  const {
34
- fetch_implementation,
34
+ fetch,
35
35
  app_reference,
36
36
  config,
37
37
  session_hash,
@@ -56,7 +56,7 @@ export function submit(
56
56
  );
57
57
 
58
58
  let websocket: WebSocket;
59
- let event_source: EventSource | null;
59
+ let stream: EventSource | null;
60
60
  let protocol = config.protocol ?? "ws";
61
61
 
62
62
  const _endpoint = typeof endpoint === "number" ? "/predict" : endpoint;
@@ -125,7 +125,7 @@ export function submit(
125
125
  }
126
126
  cancel_request = { fn_index, session_hash };
127
127
  } else {
128
- event_source?.close();
128
+ stream?.close();
129
129
  cancel_request = { event_id };
130
130
  }
131
131
 
@@ -134,7 +134,7 @@ export function submit(
134
134
  throw new Error("Could not resolve app config");
135
135
  }
136
136
 
137
- await fetch_implementation(`${config.root}/reset`, {
137
+ await fetch(`${config.root}/reset`, {
138
138
  headers: { "Content-Type": "application/json" },
139
139
  method: "POST",
140
140
  body: JSON.stringify(cancel_request)
@@ -155,7 +155,7 @@ export function submit(
155
155
  }
156
156
  }
157
157
 
158
- this.handle_blob(`${config.root}`, data, endpoint_info).then(
158
+ this.handle_blob(config.root, data, endpoint_info).then(
159
159
  async (_payload) => {
160
160
  payload = {
161
161
  data: _payload || [],
@@ -368,15 +368,19 @@ export function submit(
368
368
  }${params}`
369
369
  );
370
370
 
371
- event_source = this.eventSource_factory(url);
371
+ if (this.jwt) {
372
+ url.searchParams.set("__sign", this.jwt);
373
+ }
372
374
 
373
- if (!event_source) {
374
- throw new Error(
375
- "Cannot connect to sse endpoint: " + url.toString()
375
+ stream = this.stream_factory(url);
376
+
377
+ if (!stream) {
378
+ return Promise.reject(
379
+ new Error("Cannot connect to SSE endpoint: " + url.toString())
376
380
  );
377
381
  }
378
382
 
379
- event_source.onmessage = async function (event) {
383
+ stream.onmessage = async function (event: MessageEvent) {
380
384
  const _data = JSON.parse(event.data);
381
385
  const { type, status, data } = handle_message(
382
386
  _data,
@@ -393,7 +397,7 @@ export function submit(
393
397
  ...status
394
398
  });
395
399
  if (status.stage === "error") {
396
- event_source?.close();
400
+ stream?.close();
397
401
  }
398
402
  } else if (type === "data") {
399
403
  event_id = _data.event_id as string;
@@ -412,7 +416,7 @@ export function submit(
412
416
  fn_index,
413
417
  time: new Date()
414
418
  });
415
- event_source?.close();
419
+ stream?.close();
416
420
  }
417
421
  } else if (type === "complete") {
418
422
  complete = status;
@@ -456,7 +460,7 @@ export function submit(
456
460
  endpoint: _endpoint,
457
461
  fn_index
458
462
  });
459
- event_source?.close();
463
+ stream?.close();
460
464
  }
461
465
  }
462
466
  };
@@ -476,7 +480,11 @@ export function submit(
476
480
  fn_index,
477
481
  time: new Date()
478
482
  });
479
- let hostname = window.location.hostname;
483
+ let hostname = "";
484
+ if (typeof window !== "undefined") {
485
+ hostname = window?.location?.hostname;
486
+ }
487
+
480
488
  let hfhubdev = "dev.spaces.huggingface.tech";
481
489
  const origin = hostname.includes(".dev.")
482
490
  ? `https://moon-${hostname.split(".")[1]}.${hfhubdev}`
@@ -623,7 +631,7 @@ export function submit(
623
631
  time: new Date()
624
632
  });
625
633
  if (["sse_v2", "sse_v2.1"].includes(protocol)) {
626
- close_stream(stream_status, event_source);
634
+ close_stream(stream_status, stream);
627
635
  stream_status.open = false;
628
636
  }
629
637
  }
@@ -1,5 +1,5 @@
1
1
  import type { Client } from "..";
2
- import { BROKEN_CONNECTION_MSG } from "../constants";
2
+ import { BROKEN_CONNECTION_MSG, UPLOAD_URL } from "../constants";
3
3
  import type { UploadResponse } from "../types";
4
4
 
5
5
  export async function upload_files(
@@ -11,11 +11,14 @@ export async function upload_files(
11
11
  const headers: {
12
12
  Authorization?: string;
13
13
  } = {};
14
- if (this.options.hf_token) {
14
+ if (this?.options?.hf_token) {
15
15
  headers.Authorization = `Bearer ${this.options.hf_token}`;
16
16
  }
17
+
17
18
  const chunkSize = 1000;
18
19
  const uploadResponses = [];
20
+ let response: Response;
21
+
19
22
  for (let i = 0; i < files.length; i += chunkSize) {
20
23
  const chunk = files.slice(i, i + chunkSize);
21
24
  const formData = new FormData();
@@ -25,17 +28,19 @@ export async function upload_files(
25
28
  try {
26
29
  const upload_url = upload_id
27
30
  ? `${root_url}/upload?upload_id=${upload_id}`
28
- : `${root_url}/upload`;
29
- var response = await this.fetch_implementation(upload_url, {
31
+ : `${root_url}/${UPLOAD_URL}`;
32
+
33
+ response = await this.fetch(upload_url, {
30
34
  method: "POST",
31
35
  body: formData,
32
36
  headers
33
37
  });
34
38
  } catch (e) {
35
- return { error: BROKEN_CONNECTION_MSG };
39
+ throw new Error(BROKEN_CONNECTION_MSG + (e as Error).message);
36
40
  }
37
41
  if (!response.ok) {
38
- return { error: await response.text() };
42
+ const error_text = await response.text();
43
+ return { error: `HTTP ${response.status}: ${error_text}` };
39
44
  }
40
45
  const output: UploadResponse["files"] = await response.json();
41
46
  if (output) {
@@ -28,7 +28,7 @@ export async function view_api(this: Client): Promise<any> {
28
28
  let response: Response;
29
29
 
30
30
  if (semiver(config?.version || "2.0.0", "3.30") < 0) {
31
- response = await this.fetch_implementation(SPACE_FETCHER_URL, {
31
+ response = await this.fetch(SPACE_FETCHER_URL, {
32
32
  method: "POST",
33
33
  body: JSON.stringify({
34
34
  serialize: false,
@@ -37,12 +37,9 @@ export async function view_api(this: Client): Promise<any> {
37
37
  headers
38
38
  });
39
39
  } else {
40
- response = await this.fetch_implementation(
41
- `${config?.root}/${API_INFO_URL}`,
42
- {
43
- headers
44
- }
45
- );
40
+ response = await this.fetch(`${config?.root}/${API_INFO_URL}`, {
41
+ headers
42
+ });
46
43
  }
47
44
 
48
45
  if (!response.ok) {
package/vite.config.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { defineConfig } from "vite";
2
2
  import { svelte } from "@sveltejs/vite-plugin-svelte";
3
3
 
4
+ const TEST_MODE = process.env.TEST_MODE || "happy-dom";
5
+
4
6
  export default defineConfig({
5
7
  build: {
6
8
  lib: {
@@ -17,6 +19,11 @@ export default defineConfig({
17
19
  },
18
20
  plugins: [svelte()],
19
21
 
22
+ mode: process.env.MODE || "development",
23
+ test: {
24
+ include: ["./src/test/*.test.*"],
25
+ environment: TEST_MODE
26
+ },
20
27
  ssr: {
21
28
  target: "node",
22
29
  format: "esm",
@@ -1,173 +0,0 @@
1
- import { test, describe, assert } from "vitest";
2
- import { readFileSync } from "fs";
3
- import { join, dirname } from "path";
4
- import { fileURLToPath } from "url";
5
- import { Blob } from "node:buffer";
6
-
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
- const image_path = join(
9
- __dirname,
10
- "..",
11
- "..",
12
- "..",
13
- "demo",
14
- "kitchen_sink",
15
- "files",
16
- "lion.jpg"
17
- );
18
-
19
- import { walk_and_store_blobs } from "../helpers/data";
20
- import { Client } from "..";
21
-
22
- describe.skip("extract blob parts", () => {
23
- test("convert Buffer to Blob", async () => {
24
- const image = readFileSync(image_path);
25
- await Client.connect("gradio/hello_world_main");
26
- const parts = walk_and_store_blobs({
27
- data: {
28
- image
29
- }
30
- });
31
-
32
- assert.isTrue(parts[0].blob instanceof Blob);
33
- });
34
-
35
- test("leave node Blob as Blob", async () => {
36
- const image = new Blob([readFileSync(image_path)]);
37
-
38
- await Client.connect("gradio/hello_world_main");
39
- const parts = walk_and_store_blobs({
40
- data: {
41
- image
42
- }
43
- });
44
-
45
- assert.isTrue(parts[0].blob instanceof Blob);
46
- });
47
-
48
- test("handle deep structures", async () => {
49
- const image = new Blob([readFileSync(image_path)]);
50
-
51
- await Client.connect("gradio/hello_world_main");
52
- const parts = walk_and_store_blobs({
53
- a: {
54
- b: {
55
- data: {
56
- image
57
- }
58
- }
59
- }
60
- });
61
-
62
- assert.isTrue(parts[0].blob instanceof Blob);
63
- });
64
-
65
- test("handle deep structures with arrays", async () => {
66
- const image = new Blob([readFileSync(image_path)]);
67
-
68
- await Client.connect("gradio/hello_world_main");
69
- const parts = walk_and_store_blobs({
70
- a: [
71
- {
72
- b: [
73
- {
74
- data: [
75
- {
76
- image
77
- }
78
- ]
79
- }
80
- ]
81
- }
82
- ]
83
- });
84
-
85
- assert.isTrue(parts[0].blob instanceof Blob);
86
- });
87
-
88
- test("handle deep structures with arrays 2", async () => {
89
- const image = new Blob([readFileSync(image_path)]);
90
-
91
- await Client.connect("gradio/hello_world_main");
92
- const obj = {
93
- a: [
94
- {
95
- b: [
96
- {
97
- data: [[image], image, [image, [image]]]
98
- }
99
- ]
100
- }
101
- ]
102
- };
103
- const parts = walk_and_store_blobs(obj);
104
-
105
- function map_path(
106
- obj: Record<string, any>,
107
- parts: { path: string[]; blob: any }[]
108
- ) {
109
- const { path, blob } = parts[parts.length - 1];
110
- let ref = obj;
111
- path.forEach((p) => (ref = ref[p]));
112
-
113
- return ref === blob;
114
- }
115
-
116
- assert.isTrue(parts[0].blob instanceof Blob);
117
- // assert.isTrue(map_path(obj, parts));
118
- });
119
- });
120
-
121
- describe("handle_blob", () => {
122
- test("handle blobs", async () => {
123
- const image = new Blob([readFileSync(image_path)]);
124
-
125
- const app = await Client.connect("gradio/hello_world_main");
126
- const obj = [
127
- {
128
- a: [
129
- {
130
- b: [
131
- {
132
- data: [[image], image, [image, [image]]]
133
- }
134
- ]
135
- }
136
- ]
137
- }
138
- ];
139
-
140
- const parts = await app.handle_blob(app.config.root, obj, undefined);
141
- //@ts-ignore
142
- // assert.isString(parts.data[0].a[0].b[0].data[0][0]);
143
- });
144
- });
145
-
146
- describe.skip("private space", () => {
147
- test("can access a private space", async () => {
148
- const image = new Blob([readFileSync(image_path)]);
149
-
150
- const app = await Client.connect("pngwn/hello_world", {
151
- hf_token: "hf_"
152
- });
153
-
154
- console.log(app);
155
- const obj = [
156
- {
157
- a: [
158
- {
159
- b: [
160
- {
161
- data: [[image], image, [image, [image]]]
162
- }
163
- ]
164
- }
165
- ]
166
- }
167
- ];
168
-
169
- const parts = await app.handle_blob(app.config.root, obj, undefined);
170
- //@ts-ignore
171
- assert.isString(parts.data[0].a[0].b[0].data[0][0]);
172
- });
173
- });