chore: import upstream snapshot with attribution
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
||||
# `gradio_client`: Use a Gradio app as an API -- in 3 lines of Python
|
||||
|
||||
This directory contains the source code for `gradio_client`, a lightweight Python library that makes it very easy to use any Gradio app as an API.
|
||||
|
||||
As an example, consider this [Hugging Face Space that transcribes audio files](https://huggingface.co/spaces/abidlabs/whisper) that are recorded from the microphone.
|
||||
|
||||

|
||||
|
||||
Using the `gradio_client` library, we can easily use the Gradio as an API to transcribe audio files programmatically.
|
||||
|
||||
Here's the entire code to do it:
|
||||
|
||||
```python
|
||||
from gradio_client import Client
|
||||
|
||||
client = Client("abidlabs/whisper")
|
||||
client.predict("audio_sample.wav")
|
||||
|
||||
>> "This is a test of the whisper speech recognition model."
|
||||
```
|
||||
|
||||
The Gradio client works with any Gradio Space, whether it be an image generator, a stateful chatbot, or a tax calculator.
|
||||
|
||||
## Installation
|
||||
|
||||
If you already have a recent version of `gradio`, then the `gradio_client` is included as a dependency.
|
||||
|
||||
Otherwise, the lightweight `gradio_client` package can be installed from pip (or pip3) and works with Python versions 3.10 or higher:
|
||||
|
||||
```bash
|
||||
$ pip install gradio_client
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Connecting to a Space or a Gradio app
|
||||
|
||||
Start by connecting instantiating a `Client` object and connecting it to a Gradio app that is running on Spaces (or anywhere else)!
|
||||
|
||||
**Connecting to a Space**
|
||||
|
||||
```python
|
||||
from gradio_client import Client
|
||||
|
||||
client = Client("abidlabs/en2fr") # a Space that translates from English to French
|
||||
```
|
||||
|
||||
You can also connect to private Spaces by passing in your HF token with the `hf_token` parameter. You can get your HF token here: https://huggingface.co/settings/tokens
|
||||
|
||||
```python
|
||||
from gradio_client import Client
|
||||
|
||||
client = Client("abidlabs/my-private-space", hf_token="...")
|
||||
```
|
||||
|
||||
**Duplicating a Space for private use**
|
||||
|
||||
While you can use any public Space as an API, you may get rate limited by Hugging Face if you make too many requests. For unlimited usage of a Space, simply duplicate the Space to create a private Space,
|
||||
and then use it to make as many requests as you'd like!
|
||||
|
||||
The `gradio_client` includes a class method: `Client.duplicate()` to make this process simple:
|
||||
|
||||
```python
|
||||
from gradio_client import Client
|
||||
|
||||
client = Client.duplicate("abidlabs/whisper")
|
||||
client.predict("audio_sample.wav")
|
||||
|
||||
>> "This is a test of the whisper speech recognition model."
|
||||
```
|
||||
|
||||
If you have previously duplicated a Space, re-running `duplicate()` will _not_ create a new Space. Instead, the Client will attach to the previously-created Space. So it is safe to re-run the `Client.duplicate()` method multiple times.
|
||||
|
||||
**Note:** if the original Space uses GPUs, your private Space will as well, and your Hugging Face account will get billed based on the price of the GPU. To minimize charges, your Space will automatically go to sleep after 1 hour of inactivity. You can also set the hardware using the `hardware` parameter of `duplicate()`.
|
||||
|
||||
**Connecting a general Gradio app**
|
||||
|
||||
If your app is running somewhere else, just provide the full URL instead, including the "http://" or "https://". Here's an example of making predictions to a Gradio app that is running on a share URL:
|
||||
|
||||
```python
|
||||
from gradio_client import Client
|
||||
|
||||
client = Client("https://bec81a83-5b5c-471e.gradio.live")
|
||||
```
|
||||
|
||||
### Inspecting the API endpoints
|
||||
|
||||
Once you have connected to a Gradio app, you can view the APIs that are available to you by calling the `.view_api()` method. For the Whisper Space, we see the following:
|
||||
|
||||
```
|
||||
Client.predict() Usage Info
|
||||
---------------------------
|
||||
Named API endpoints: 1
|
||||
|
||||
- predict(input_audio, api_name="/predict") -> value_0
|
||||
Parameters:
|
||||
- [Audio] input_audio: str (filepath or URL)
|
||||
Returns:
|
||||
- [Textbox] value_0: str (value)
|
||||
```
|
||||
|
||||
This shows us that we have 1 API endpoint in this space, and shows us how to use the API endpoint to make a prediction: we should call the `.predict()` method, providing a parameter `input_audio` of type `str`, which is a `filepath or URL`.
|
||||
|
||||
We should also provide the `api_name='/predict'` argument. Although this isn't necessary if a Gradio app has a single named endpoint, it does allow us to call different endpoints in a single app if they are available. If an app has unnamed API endpoints, these can also be displayed by running `.view_api(all_endpoints=True)`.
|
||||
|
||||
### Making a prediction
|
||||
|
||||
The simplest way to make a prediction is simply to call the `.predict()` function with the appropriate arguments:
|
||||
|
||||
```python
|
||||
from gradio_client import Client
|
||||
|
||||
client = Client("abidlabs/en2fr")
|
||||
client.predict("Hello")
|
||||
|
||||
>> Bonjour
|
||||
```
|
||||
|
||||
If there are multiple parameters, then you should pass them as separate arguments to `.predict()`, like this:
|
||||
|
||||
```python
|
||||
from gradio_client import Client
|
||||
|
||||
client = Client("gradio/calculator")
|
||||
client.predict(4, "add", 5)
|
||||
|
||||
>> 9.0
|
||||
```
|
||||
|
||||
For certain inputs, such as images, you should pass in the filepath or URL to the file. Likewise, for the corresponding output types, you will get a filepath or URL returned.
|
||||
|
||||
```python
|
||||
from gradio_client import Client
|
||||
|
||||
client = Client("abidlabs/whisper")
|
||||
client.predict("https://audio-samples.github.io/samples/mp3/blizzard_unconditional/sample-0.mp3")
|
||||
|
||||
>> "My thought I have nobody by a beauty and will as you poured. Mr. Rochester is serve in that so don't find simpus, and devoted abode, to at might in a r—"
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
For more ways to use the Gradio Python Client, check out our dedicated Guide on the Python client, available here: https://www.gradio.app/guides/getting-started-with-the-python-client
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname ${0})"
|
||||
|
||||
python3 -m pip install build
|
||||
rm -rf dist/*
|
||||
rm -rf build/*
|
||||
python3 -m build
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
from gradio_client.client import Client
|
||||
from gradio_client.data_classes import FileData
|
||||
from gradio_client.utils import __version__, file, handle_file
|
||||
|
||||
__all__ = [
|
||||
"Client",
|
||||
"file",
|
||||
"handle_file",
|
||||
"FileData",
|
||||
"__version__",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
|
||||
class FileData(TypedDict):
|
||||
name: str | None # filename
|
||||
data: str | None # base64 encoded data
|
||||
size: NotRequired[int | None] # size in bytes
|
||||
is_file: NotRequired[
|
||||
bool
|
||||
] # whether the data corresponds to a file or base64 encoded data
|
||||
orig_name: NotRequired[str] # original filename
|
||||
mime_type: NotRequired[str]
|
||||
is_stream: NotRequired[bool]
|
||||
|
||||
|
||||
class ParameterInfo(TypedDict):
|
||||
label: str
|
||||
parameter_name: str
|
||||
parameter_has_default: NotRequired[bool]
|
||||
parameter_default: NotRequired[Any]
|
||||
type: dict
|
||||
python_type: dict
|
||||
component: str
|
||||
example_input: Any
|
||||
@@ -0,0 +1,371 @@
|
||||
"""Contains methods that generate documentation for Gradio functions and classes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import inspect
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
|
||||
classes_to_document = defaultdict(list)
|
||||
classes_inherit_documentation = {}
|
||||
|
||||
|
||||
def set_documentation_group(m): # noqa: ARG001
|
||||
"""A no-op for backwards compatibility of custom components published prior to 4.16.0"""
|
||||
pass
|
||||
|
||||
|
||||
def extract_instance_attr_doc(cls, attr):
|
||||
code = inspect.getsource(cls.__init__)
|
||||
lines = [line.strip() for line in code.split("\n")]
|
||||
i = None
|
||||
for i, line in enumerate(lines): # noqa: B007
|
||||
if line.startswith("self." + attr + ":") or line.startswith(
|
||||
"self." + attr + " ="
|
||||
):
|
||||
break
|
||||
if i is None:
|
||||
raise NameError(f"Could not find {attr} in {cls.__name__}")
|
||||
start_line = lines.index('"""', i)
|
||||
end_line = lines.index('"""', start_line + 1)
|
||||
for j in range(i + 1, start_line):
|
||||
if lines[j].startswith("self."):
|
||||
raise ValueError(
|
||||
f"Found another attribute before docstring for {attr} in {cls.__name__}: "
|
||||
+ lines[j]
|
||||
+ "\n start:"
|
||||
+ lines[i]
|
||||
)
|
||||
doc_string = " ".join(lines[start_line + 1 : end_line])
|
||||
return doc_string
|
||||
|
||||
|
||||
_module_prefixes = [
|
||||
("gradio._simple_templates", "component"),
|
||||
("gradio.server", "block"),
|
||||
("gradio.block", "block"),
|
||||
("gradio.chat", "chatinterface"),
|
||||
("gradio.component", "component"),
|
||||
("gradio.events", "helpers"),
|
||||
("gradio.data_classes", "helpers"),
|
||||
("gradio.exceptions", "helpers"),
|
||||
("gradio.external", "helpers"),
|
||||
("gradio.flag", "flagging"),
|
||||
("gradio.helpers", "helpers"),
|
||||
("gradio.interface", "interface"),
|
||||
("gradio.layout", "layout"),
|
||||
("gradio.route", "routes"),
|
||||
("gradio.theme", "themes"),
|
||||
("gradio_client.", "py-client"),
|
||||
("gradio.utils", "helpers"),
|
||||
("gradio.renderable", "renderable"),
|
||||
("gradio.validators", "validators"),
|
||||
("gradio.caching", "helpers"),
|
||||
]
|
||||
|
||||
|
||||
@lru_cache(maxsize=10)
|
||||
def _get_module_documentation_group(modname) -> str:
|
||||
for prefix, group in _module_prefixes:
|
||||
if modname.startswith(prefix):
|
||||
return group
|
||||
raise ValueError(f"No known documentation group for module {modname!r}")
|
||||
|
||||
|
||||
def document(*fns, inherit=False, documentation_group=None):
|
||||
"""
|
||||
Defines the @document decorator which adds classes or functions to the Gradio
|
||||
documentation at www.gradio.app/docs.
|
||||
|
||||
Usage examples:
|
||||
- Put @document() above a class to document the class and its constructor.
|
||||
- Put @document("fn1", "fn2") above a class to also document methods fn1 and fn2.
|
||||
- Put @document("*fn3") with an asterisk above a class to document the instance attribute methods f3.
|
||||
"""
|
||||
_documentation_group = documentation_group
|
||||
|
||||
def inner_doc(cls):
|
||||
functions = list(fns)
|
||||
if hasattr(cls, "EVENTS"):
|
||||
functions += cls.EVENTS
|
||||
if inherit:
|
||||
classes_inherit_documentation[cls] = None
|
||||
|
||||
documentation_group = _documentation_group # avoid `nonlocal` reassignment
|
||||
if _documentation_group is None:
|
||||
try:
|
||||
modname = inspect.getmodule(cls).__name__ # type: ignore
|
||||
if modname.startswith("gradio.") or modname.startswith(
|
||||
"gradio_client."
|
||||
):
|
||||
documentation_group = _get_module_documentation_group(modname)
|
||||
else:
|
||||
# Then this is likely a custom Gradio component that we do not include in the documentation
|
||||
pass
|
||||
except Exception as exc:
|
||||
warnings.warn(f"Could not get documentation group for {cls}: {exc}")
|
||||
classes_to_document[documentation_group].append((cls, functions))
|
||||
return cls
|
||||
|
||||
return inner_doc
|
||||
|
||||
|
||||
def document_fn(fn: Callable, cls) -> tuple[str, list[dict], dict, str | None]:
|
||||
"""
|
||||
Generates documentation for any function.
|
||||
Parameters:
|
||||
fn: Function to document
|
||||
Returns:
|
||||
description: General description of fn
|
||||
parameters: A list of dicts for each parameter, storing data for the parameter name, annotation and doc
|
||||
return: A dict storing data for the returned annotation and doc
|
||||
example: Code for an example use of the fn
|
||||
"""
|
||||
doc_str = inspect.getdoc(fn) or ""
|
||||
doc_lines = doc_str.split("\n")
|
||||
signature = inspect.signature(fn)
|
||||
description, parameters, returns, examples = [], {}, [], []
|
||||
mode = "description"
|
||||
current_parameter = None
|
||||
base_indent = None
|
||||
for line in doc_lines:
|
||||
line = line.rstrip()
|
||||
if line == "Parameters:":
|
||||
mode = "parameter"
|
||||
base_indent = None
|
||||
elif line.startswith("Example:"):
|
||||
mode = "example"
|
||||
base_indent = None
|
||||
if "(" in line and ")" in line:
|
||||
c = line.split("(")[1].split(")")[0]
|
||||
if c != cls.__name__:
|
||||
mode = "ignore"
|
||||
elif line == "Returns:":
|
||||
mode = "return"
|
||||
base_indent = None
|
||||
else:
|
||||
if mode == "description":
|
||||
description.append(line if line.strip() else "<br>")
|
||||
continue
|
||||
if not (line.startswith(" ") or line.strip() == ""):
|
||||
print(line)
|
||||
if not (line.startswith(" ") or line.strip() == ""):
|
||||
raise SyntaxError(
|
||||
f"Documentation format for {fn.__name__} has format error in line: {line}"
|
||||
)
|
||||
line = line[4:]
|
||||
if mode == "parameter":
|
||||
if ": " in line and not line.startswith(" "):
|
||||
colon_index = line.index(": ")
|
||||
if colon_index < -1:
|
||||
raise SyntaxError(
|
||||
f"Documentation format for {fn.__name__} has format error in line: {line}"
|
||||
)
|
||||
current_parameter = line[:colon_index]
|
||||
parameter_doc = line[colon_index + 2 :]
|
||||
parameters[current_parameter] = parameter_doc
|
||||
base_indent = None
|
||||
elif current_parameter and line.strip():
|
||||
if base_indent is None:
|
||||
base_indent = len(line) - len(line.lstrip())
|
||||
if base_indent > 0 and line.startswith(" " * base_indent):
|
||||
line = line[base_indent:]
|
||||
parameters[current_parameter] += "\n" + line
|
||||
elif mode == "return":
|
||||
returns.append(line)
|
||||
elif mode == "example":
|
||||
examples.append(line)
|
||||
description_doc = " ".join(description)
|
||||
parameter_docs = []
|
||||
for param_name, param in signature.parameters.items():
|
||||
if param_name.startswith("_"):
|
||||
continue
|
||||
if param_name == "self":
|
||||
continue
|
||||
if param_name in ["kwargs", "args"] and param_name not in parameters:
|
||||
continue
|
||||
parameter_doc = {
|
||||
"name": param_name,
|
||||
"annotation": param.annotation,
|
||||
"doc": parameters.get(param_name),
|
||||
}
|
||||
if param_name in parameters:
|
||||
del parameters[param_name]
|
||||
if param.default != inspect.Parameter.empty:
|
||||
default = param.default
|
||||
if isinstance(default, str):
|
||||
default = '"' + default + '"'
|
||||
if default.__class__.__module__ != "builtins":
|
||||
default = f"{default.__class__.__name__}()"
|
||||
parameter_doc["default"] = default
|
||||
elif parameter_doc["doc"] is not None:
|
||||
if "kwargs" in parameter_doc["doc"]:
|
||||
parameter_doc["kwargs"] = True
|
||||
if "args" in parameter_doc["doc"]:
|
||||
parameter_doc["args"] = True
|
||||
parameter_docs.append(parameter_doc)
|
||||
if parameters:
|
||||
raise ValueError(
|
||||
f"Documentation format for {fn.__name__} documents "
|
||||
f"nonexistent parameters: {', '.join(parameters.keys())}. "
|
||||
f"Valid parameters: {', '.join(signature.parameters.keys())}"
|
||||
)
|
||||
if len(returns) == 0:
|
||||
return_docs = {}
|
||||
else:
|
||||
return_doc_text = "\n".join(returns)
|
||||
return_docs = {
|
||||
"annotation": signature.return_annotation,
|
||||
"doc": return_doc_text,
|
||||
}
|
||||
examples_doc = "\n".join(examples) if len(examples) > 0 else None
|
||||
return description_doc, parameter_docs, return_docs, examples_doc
|
||||
|
||||
|
||||
def document_cls(cls):
|
||||
doc_str = inspect.getdoc(cls)
|
||||
if doc_str is None:
|
||||
return "", {}, ""
|
||||
tags = {}
|
||||
description_lines = []
|
||||
mode = "description"
|
||||
for line in doc_str.split("\n"):
|
||||
line = line.rstrip()
|
||||
if line.endswith(":") and " " not in line:
|
||||
mode = line[:-1].lower()
|
||||
tags[mode] = []
|
||||
elif line.split(" ")[0].endswith(":") and not line.startswith(" "):
|
||||
tag = line[: line.index(":")].lower()
|
||||
value = line[line.index(":") + 2 :]
|
||||
tags[tag] = value
|
||||
elif mode == "description":
|
||||
description_lines.append(line if line.strip() else "<br>")
|
||||
else:
|
||||
if not (line.startswith(" ") or not line.strip()):
|
||||
raise SyntaxError(
|
||||
f"Documentation format for {cls.__name__} has format error in line: {line}"
|
||||
)
|
||||
tags[mode].append(line[4:])
|
||||
if "example" in tags:
|
||||
example = "\n".join(tags["example"])
|
||||
del tags["example"]
|
||||
else:
|
||||
example = None
|
||||
for key, val in tags.items():
|
||||
if isinstance(val, list):
|
||||
tags[key] = "<br>".join(val)
|
||||
description = " ".join(description_lines).replace("\n", "<br>")
|
||||
return description, tags, example
|
||||
|
||||
|
||||
def generate_documentation():
|
||||
documentation = {}
|
||||
for mode, class_list in classes_to_document.items():
|
||||
documentation[mode] = []
|
||||
for cls, fns in class_list:
|
||||
fn_to_document = (
|
||||
cls
|
||||
if inspect.isfunction(cls) or dataclasses.is_dataclass(cls)
|
||||
else cls.__init__
|
||||
)
|
||||
_, parameter_doc, return_doc, _ = document_fn(fn_to_document, cls)
|
||||
if (
|
||||
hasattr(cls, "preprocess")
|
||||
and callable(cls.preprocess) # type: ignore
|
||||
and hasattr(cls, "postprocess")
|
||||
and callable(cls.postprocess) # type: ignore
|
||||
):
|
||||
preprocess_doc = document_fn(cls.preprocess, cls) # type: ignore
|
||||
postprocess_doc = document_fn(cls.postprocess, cls) # type: ignore
|
||||
preprocess_doc, postprocess_doc = (
|
||||
{
|
||||
"parameter_doc": preprocess_doc[1],
|
||||
"return_doc": preprocess_doc[2],
|
||||
},
|
||||
{
|
||||
"parameter_doc": postprocess_doc[1],
|
||||
"return_doc": postprocess_doc[2],
|
||||
},
|
||||
)
|
||||
cls_description, cls_tags, cls_example = document_cls(cls)
|
||||
cls_documentation = {
|
||||
"class": cls,
|
||||
"name": cls.__name__,
|
||||
"description": cls_description,
|
||||
"tags": cls_tags,
|
||||
"parameters": parameter_doc,
|
||||
"returns": return_doc,
|
||||
"example": cls_example,
|
||||
"fns": [],
|
||||
}
|
||||
if (
|
||||
hasattr(cls, "preprocess")
|
||||
and callable(cls.preprocess) # type: ignore
|
||||
and hasattr(cls, "postprocess")
|
||||
and callable(cls.postprocess) # type: ignore
|
||||
):
|
||||
cls_documentation["preprocess"] = preprocess_doc # type: ignore
|
||||
cls_documentation["postprocess"] = postprocess_doc # type: ignore
|
||||
for fn_name in fns:
|
||||
instance_attribute_fn = fn_name.startswith("*")
|
||||
if instance_attribute_fn:
|
||||
fn_name = fn_name[1:]
|
||||
# Instance attribute fns are classes
|
||||
# whose __call__ method determines their behavior
|
||||
fn = getattr(cls(), fn_name).__call__
|
||||
else:
|
||||
fn = getattr(cls, fn_name)
|
||||
if not callable(fn):
|
||||
description_doc = str(fn)
|
||||
parameter_docs = {}
|
||||
return_docs = {}
|
||||
examples_doc = ""
|
||||
override_signature = f"gr.{cls.__name__}.{fn_name}"
|
||||
else:
|
||||
(
|
||||
description_doc,
|
||||
parameter_docs,
|
||||
return_docs,
|
||||
examples_doc,
|
||||
) = document_fn(fn, cls)
|
||||
if fn_name in getattr(cls, "EVENTS", []):
|
||||
parameter_docs = parameter_docs[1:]
|
||||
override_signature = None
|
||||
if instance_attribute_fn:
|
||||
description_doc = extract_instance_attr_doc(cls, fn_name)
|
||||
cls_documentation["fns"].append(
|
||||
{
|
||||
"fn": fn,
|
||||
"name": fn_name,
|
||||
"description": description_doc,
|
||||
"tags": {},
|
||||
"parameters": parameter_docs,
|
||||
"returns": return_docs,
|
||||
"example": examples_doc,
|
||||
"override_signature": override_signature,
|
||||
}
|
||||
)
|
||||
documentation[mode].append(cls_documentation)
|
||||
if cls in classes_inherit_documentation:
|
||||
classes_inherit_documentation[cls] = cls_documentation["fns"]
|
||||
for mode, class_list in classes_to_document.items():
|
||||
for i, (cls, _) in enumerate(class_list):
|
||||
for super_class, fns in classes_inherit_documentation.items():
|
||||
if (
|
||||
inspect.isclass(cls)
|
||||
and issubclass(cls, super_class)
|
||||
and cls != super_class
|
||||
):
|
||||
for inherited_fn in fns:
|
||||
inherited_fn = dict(inherited_fn)
|
||||
try:
|
||||
inherited_fn["description"] = extract_instance_attr_doc(
|
||||
cls, inherited_fn["name"]
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
documentation[mode][i]["fns"].append(inherited_fn)
|
||||
return documentation
|
||||
@@ -0,0 +1,43 @@
|
||||
class SerializationSetupError(ValueError):
|
||||
"""Raised when a serializers cannot be set up correctly."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AuthenticationError(ValueError):
|
||||
"""Raised when the client is unable to authenticate itself to a Gradio app due to invalid or missing credentials."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(ValueError):
|
||||
"""Raised when the data that is passed into the Gradio app fails developer-defined validation."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AppError(ValueError):
|
||||
"""Raised when the upstream Gradio app throws an error because of the value submitted by the client."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Error raised.",
|
||||
duration: float | None = 10,
|
||||
visible: bool = True,
|
||||
title: str = "Error",
|
||||
print_exception: bool = True,
|
||||
):
|
||||
"""
|
||||
Parameters:
|
||||
message: The error message to be displayed to the user. Can be HTML, which will be rendered in the modal.
|
||||
duration: The duration in seconds to display the error message. If None or 0, the error message will be displayed until the user closes it.
|
||||
visible: Whether the error message should be displayed in the UI.
|
||||
title: The title to be displayed to the user at the top of the error modal.
|
||||
print_exception: Whether to print traceback of the error to the console when the error is raised.
|
||||
"""
|
||||
self.title = title
|
||||
self.message = message
|
||||
self.duration = duration
|
||||
self.visible = visible
|
||||
self.print_exception = print_exception
|
||||
super().__init__(self.message)
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "gradio_client",
|
||||
"version": "2.5.0",
|
||||
"description": "",
|
||||
"python": "true",
|
||||
"main_changeset": true,
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
"""Centralized code snippet generation for Gradio API endpoints. Generates Python, JavaScript, and cURL code snippets from API info dicts."""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
BLOB_COMPONENTS = {
|
||||
"Audio",
|
||||
"DownloadButton",
|
||||
"File",
|
||||
"Image",
|
||||
"ImageSlider",
|
||||
"Model3D",
|
||||
"UploadButton",
|
||||
"Video",
|
||||
}
|
||||
|
||||
|
||||
def _is_file_data(obj: Any) -> bool:
|
||||
return (
|
||||
isinstance(obj, dict)
|
||||
and "url" in obj
|
||||
and obj.get("url")
|
||||
and "meta" in obj
|
||||
and isinstance(obj.get("meta"), dict)
|
||||
and obj["meta"].get("_type") == "gradio.FileData"
|
||||
)
|
||||
|
||||
|
||||
def _has_file_data(obj: Any) -> bool:
|
||||
if isinstance(obj, dict):
|
||||
if _is_file_data(obj):
|
||||
return True
|
||||
return any(_has_file_data(v) for v in obj.values())
|
||||
if isinstance(obj, (list, tuple)):
|
||||
return any(_has_file_data(item) for item in obj)
|
||||
return False
|
||||
|
||||
|
||||
def _replace_file_data_py(obj: Any) -> Any:
|
||||
if isinstance(obj, dict) and _is_file_data(obj):
|
||||
return f"handle_file('{obj['url']}')"
|
||||
if isinstance(obj, (list, tuple)):
|
||||
return [_replace_file_data_py(item) for item in obj]
|
||||
if isinstance(obj, dict):
|
||||
return {k: _replace_file_data_py(v) for k, v in obj.items()}
|
||||
return obj
|
||||
|
||||
|
||||
def _simplify_file_data(obj: Any) -> Any:
|
||||
if isinstance(obj, dict) and _is_file_data(obj):
|
||||
return {"path": obj["url"], "meta": {"_type": "gradio.FileData"}}
|
||||
if isinstance(obj, (list, tuple)):
|
||||
return [_simplify_file_data(item) for item in obj]
|
||||
if isinstance(obj, dict):
|
||||
return {k: _simplify_file_data(v) for k, v in obj.items()}
|
||||
return obj
|
||||
|
||||
|
||||
_UNQUOTED = "UNQUOTED_GRADIO_"
|
||||
|
||||
|
||||
def _stringify_py(obj: Any) -> str:
|
||||
def _prepare(o: Any) -> Any:
|
||||
if o is None:
|
||||
return f"{_UNQUOTED}None"
|
||||
if isinstance(o, bool):
|
||||
return f"{_UNQUOTED}True" if o else f"{_UNQUOTED}False"
|
||||
if isinstance(o, str) and o.startswith("handle_file(") and o.endswith(")"):
|
||||
return f"{_UNQUOTED}{o}"
|
||||
if isinstance(o, (list, tuple)):
|
||||
return [_prepare(item) for item in o]
|
||||
if isinstance(o, dict):
|
||||
return {k: _prepare(v) for k, v in o.items()}
|
||||
return o
|
||||
|
||||
prepared = _prepare(obj)
|
||||
result = json.dumps(prepared, default=str)
|
||||
result = re.sub(
|
||||
rf'"{_UNQUOTED}(handle_file\([^)]*\))"',
|
||||
r"\1",
|
||||
result,
|
||||
)
|
||||
result = result.replace(f'"{_UNQUOTED}None"', "None")
|
||||
result = result.replace(f'"{_UNQUOTED}True"', "True")
|
||||
result = result.replace(f'"{_UNQUOTED}False"', "False")
|
||||
return result
|
||||
|
||||
|
||||
def _represent_value(value: Any, python_type: str | None, lang: str) -> str:
|
||||
if python_type is None:
|
||||
return "None" if lang == "py" else "null"
|
||||
if value is None:
|
||||
return "None" if lang == "py" else "null"
|
||||
if python_type in ("string", "str"):
|
||||
return f'"{value}"'
|
||||
if python_type == "number":
|
||||
return str(value)
|
||||
if python_type in ("boolean", "bool"):
|
||||
if lang == "py":
|
||||
return "True" if value else "False"
|
||||
return str(value).lower() if isinstance(value, bool) else str(value)
|
||||
if python_type == "List[str]":
|
||||
return json.dumps(value)
|
||||
if python_type.startswith("Literal['"):
|
||||
return f'"{value}"'
|
||||
|
||||
if isinstance(value, str):
|
||||
if value == "":
|
||||
return "None" if lang == "py" else "null"
|
||||
return value
|
||||
|
||||
value = copy.deepcopy(value)
|
||||
if lang == "bash":
|
||||
value = _simplify_file_data(value)
|
||||
if lang == "py":
|
||||
value = _replace_file_data_py(value)
|
||||
return _stringify_py(value)
|
||||
|
||||
|
||||
def _get_param_value(param: dict) -> Any:
|
||||
if param.get("parameter_has_default"):
|
||||
return param.get("parameter_default")
|
||||
return param.get("example_input")
|
||||
|
||||
|
||||
def generate_python_snippet(
|
||||
api_name: str,
|
||||
params: list[dict],
|
||||
src: str,
|
||||
) -> str:
|
||||
has_file = any(_has_file_data(p.get("example_input")) for p in params)
|
||||
imports = "from gradio_client import Client"
|
||||
if has_file:
|
||||
imports += ", handle_file"
|
||||
|
||||
lines = [imports, ""]
|
||||
lines.append(f'client = Client("{src}")')
|
||||
|
||||
predict_args = []
|
||||
for p in params:
|
||||
name = p.get("parameter_name") or p.get("label", "input")
|
||||
value = _get_param_value(p)
|
||||
ptype = p.get("python_type", {}).get("type")
|
||||
formatted = _represent_value(value, ptype, "py")
|
||||
predict_args.append(f"\t{name}={formatted},")
|
||||
|
||||
lines.append("result = client.predict(")
|
||||
lines.extend(predict_args)
|
||||
lines.append(f'\tapi_name="{api_name}",')
|
||||
lines.append(")")
|
||||
lines.append("print(result)")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_js_snippet(
|
||||
api_name: str,
|
||||
params: list[dict],
|
||||
src: str,
|
||||
) -> str:
|
||||
blob_params = [p for p in params if p.get("component") in BLOB_COMPONENTS]
|
||||
|
||||
lines = ['import { Client } from "@gradio/client";', ""]
|
||||
|
||||
for i, bp in enumerate(blob_params):
|
||||
example = bp.get("example_input", {})
|
||||
url = example.get("url", "") if isinstance(example, dict) else ""
|
||||
component = bp.get("component", "")
|
||||
lines.append(f'const response_{i} = await fetch("{url}");')
|
||||
lines.append(f"const example{component} = await response_{i}.blob();")
|
||||
|
||||
if blob_params:
|
||||
lines.append("")
|
||||
|
||||
lines.append(f'const client = await Client.connect("{src}");')
|
||||
|
||||
blob_component_names = {bp.get("component") for bp in blob_params}
|
||||
|
||||
predict_args = []
|
||||
for p in params:
|
||||
name = p.get("parameter_name") or p.get("label", "input")
|
||||
component = p.get("component", "")
|
||||
if component in blob_component_names:
|
||||
predict_args.append(f"\t\t{name}: example{component},")
|
||||
else:
|
||||
value = _get_param_value(p)
|
||||
ptype = p.get("python_type", {}).get("type")
|
||||
formatted = _represent_value(value, ptype, "js")
|
||||
predict_args.append(f"\t\t{name}: {formatted},")
|
||||
|
||||
lines.append(f'const result = await client.predict("{api_name}", {{')
|
||||
lines.extend(predict_args)
|
||||
lines.append("});")
|
||||
lines.append("")
|
||||
lines.append("console.log(result.data);")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_bash_snippet(
|
||||
api_name: str,
|
||||
params: list[dict],
|
||||
root: str,
|
||||
api_prefix: str = "/",
|
||||
) -> str:
|
||||
normalised_root = root.rstrip("/")
|
||||
normalised_prefix = api_prefix if api_prefix else "/"
|
||||
endpoint_name = api_name.lstrip("/")
|
||||
|
||||
has_file = any(_has_file_data(p.get("example_input")) for p in params)
|
||||
upload_url = f"{normalised_root}{normalised_prefix}upload"
|
||||
|
||||
lines: list[str] = []
|
||||
|
||||
file_param_names: list[str] = []
|
||||
if has_file:
|
||||
for p in params:
|
||||
if _has_file_data(p.get("example_input")):
|
||||
name = p.get("parameter_name") or p.get("label", "input")
|
||||
file_param_names.append(name)
|
||||
lines.append(
|
||||
f"FILE_PATH=$(curl -s -X POST {upload_url}"
|
||||
" -F 'files=@/path/to/your/file'"
|
||||
" | tr -d '[]\" ')"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
data_dict = {}
|
||||
for p in params:
|
||||
name = p.get("parameter_name") or p.get("label", "input")
|
||||
if name in file_param_names:
|
||||
data_dict[name] = "FILE_PATH_PLACEHOLDER"
|
||||
else:
|
||||
value = _get_param_value(p)
|
||||
ptype = p.get("python_type", {}).get("type")
|
||||
formatted = _represent_value(value, ptype, "bash")
|
||||
data_dict[name] = formatted
|
||||
|
||||
data_entries = ", ".join(f'"{k}": {v}' for k, v in data_dict.items())
|
||||
data_str = "{" + data_entries + "}"
|
||||
for _ in file_param_names:
|
||||
replacement = '{"path": "\'$FILE_PATH\'", "meta": {"_type": "gradio.FileData"}}'
|
||||
data_str = data_str.replace("FILE_PATH_PLACEHOLDER", replacement)
|
||||
|
||||
base_url = f"{normalised_root}{normalised_prefix}call/v2/{endpoint_name}"
|
||||
get_url = f"{normalised_root}{normalised_prefix}call/{endpoint_name}"
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
f'curl -X POST {base_url} -s -H "Content-Type: application/json" \\',
|
||||
f" -d '{data_str}' \\",
|
||||
" | awk -F'\"' '{ print $4}' \\",
|
||||
f" | read EVENT_ID; curl -N {get_url}/$EVENT_ID",
|
||||
]
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_code_snippets(
|
||||
api_name: str,
|
||||
endpoint_info: dict,
|
||||
root: str,
|
||||
space_id: str | None = None,
|
||||
api_prefix: str = "/",
|
||||
) -> dict[str, str]:
|
||||
params = endpoint_info.get("parameters", [])
|
||||
src = space_id or root
|
||||
|
||||
return {
|
||||
"python": generate_python_snippet(api_name, params, src),
|
||||
"javascript": generate_js_snippet(api_name, params, src),
|
||||
"bash": generate_bash_snippet(api_name, params, root, api_prefix),
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import asyncio
|
||||
import os
|
||||
import threading
|
||||
from threading import Event
|
||||
|
||||
import discord
|
||||
import gradio as gr
|
||||
from discord import Permissions
|
||||
from discord.ext import commands
|
||||
from discord.utils import oauth_url
|
||||
|
||||
import gradio_client as grc
|
||||
from gradio_client.utils import QueueError
|
||||
|
||||
event = Event()
|
||||
|
||||
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
||||
|
||||
|
||||
async def wait(job):
|
||||
while not job.done():
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
|
||||
def get_client(session: str | None = None) -> grc.Client:
|
||||
client = grc.Client("<<app-src>>", token=os.getenv("HF_TOKEN"))
|
||||
if session:
|
||||
client.session_hash = session
|
||||
return client
|
||||
|
||||
|
||||
def truncate_response(response: str) -> str:
|
||||
ending = "...\nTruncating response to 2000 characters due to discord api limits."
|
||||
if len(response) > 2000:
|
||||
return response[: 2000 - len(ending)] + ending
|
||||
else:
|
||||
return response
|
||||
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
bot = commands.Bot(command_prefix="/", intents=intents)
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
print(f"Logged in as {bot.user} (ID: {bot.user.id})")
|
||||
synced = await bot.tree.sync()
|
||||
print(f"Synced commands: {', '.join([s.name for s in synced])}.")
|
||||
event.set()
|
||||
print("------")
|
||||
|
||||
|
||||
thread_to_client = {}
|
||||
thread_to_user = {}
|
||||
|
||||
|
||||
@bot.hybrid_command(
|
||||
name="<<command-name>>",
|
||||
description="Enter some text to chat with the bot! Like this: /<<command-name>> Hello, how are you?",
|
||||
)
|
||||
async def chat(ctx, prompt: str):
|
||||
if ctx.author.id == bot.user.id:
|
||||
return
|
||||
try:
|
||||
message = await ctx.send("Creating thread...")
|
||||
|
||||
thread = await message.create_thread(name=prompt)
|
||||
loop = asyncio.get_running_loop()
|
||||
client = await loop.run_in_executor(None, get_client, None)
|
||||
job = client.submit(prompt, api_name="/<<api-name>>")
|
||||
await wait(job)
|
||||
|
||||
try:
|
||||
job.result()
|
||||
response = job.outputs()[-1]
|
||||
await thread.send(truncate_response(response))
|
||||
thread_to_client[thread.id] = client
|
||||
thread_to_user[thread.id] = ctx.author.id
|
||||
except QueueError:
|
||||
await thread.send(
|
||||
"The gradio space powering this bot is really busy! Please try again later!"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"{e}")
|
||||
|
||||
|
||||
async def continue_chat(message):
|
||||
"""Continues a given conversation based on chathistory"""
|
||||
try:
|
||||
client = thread_to_client[message.channel.id]
|
||||
prompt = message.content
|
||||
job = client.submit(prompt, api_name="/<<api-name>>")
|
||||
await wait(job)
|
||||
try:
|
||||
job.result()
|
||||
response = job.outputs()[-1]
|
||||
await message.reply(truncate_response(response))
|
||||
except QueueError:
|
||||
await message.reply(
|
||||
"The gradio space powering this bot is really busy! Please try again later!"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
"""Continue the chat"""
|
||||
try:
|
||||
if not message.author.bot:
|
||||
if message.channel.id in thread_to_user:
|
||||
if thread_to_user[message.channel.id] == message.author.id:
|
||||
await continue_chat(message)
|
||||
else:
|
||||
await bot.process_commands(message)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
# running in thread
|
||||
def run_bot():
|
||||
if not DISCORD_TOKEN:
|
||||
print("DISCORD_TOKEN NOT SET")
|
||||
event.set()
|
||||
else:
|
||||
bot.run(DISCORD_TOKEN)
|
||||
|
||||
|
||||
threading.Thread(target=run_bot).start()
|
||||
|
||||
event.wait()
|
||||
|
||||
if not DISCORD_TOKEN:
|
||||
welcome_message = """
|
||||
|
||||
## You have not specified a DISCORD_TOKEN, which means you have not created a bot account. Please follow these steps:
|
||||
|
||||
### 1. Go to https://discord.com/developers/applications and click 'New Application'
|
||||
|
||||
### 2. Give your bot a name 🤖
|
||||
|
||||

|
||||
|
||||
## 3. In Settings > Bot, click the 'Reset Token' button to get a new token. Write it down and keep it safe 🔐
|
||||
|
||||

|
||||
|
||||
## 4. Optionally make the bot public if you want anyone to be able to add it to their servers
|
||||
|
||||
## 5. Scroll down and enable 'Message Content Intent' under 'Priviledged Gateway Intents'
|
||||
|
||||

|
||||
|
||||
## 6. Save your changes!
|
||||
|
||||
## 7. The token from step 3 is the DISCORD_TOKEN. Rerun the deploy_discord command, e.g client.deploy_discord(discord_bot_token=DISCORD_TOKEN, ...), or add the token as a space secret manually.
|
||||
"""
|
||||
else:
|
||||
permissions = Permissions(326417525824)
|
||||
url = oauth_url(bot.user.id, permissions=permissions)
|
||||
welcome_message = f"""
|
||||
## Add this bot to your server by clicking this link:
|
||||
|
||||
{url}
|
||||
|
||||
## How to use it?
|
||||
|
||||
The bot can be triggered via `/<<command-name>>` followed by your text prompt.
|
||||
|
||||
This will create a thread with the bot's response to your text prompt.
|
||||
You can reply in the thread (without `/<<command-name>>`) to continue the conversation.
|
||||
In the thread, the bot will only reply to the original author of the command.
|
||||
|
||||
⚠️ Note ⚠️: Please make sure this bot's command does have the same name as another command in your server.
|
||||
|
||||
⚠️ Note ⚠️: Bot commands do not work in DMs with the bot as of now.
|
||||
"""
|
||||
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
gr.Markdown(
|
||||
f"""
|
||||
# Discord bot of <<app-src>>
|
||||
{welcome_message}
|
||||
"""
|
||||
)
|
||||
|
||||
demo.launch()
|
||||
@@ -0,0 +1,199 @@
|
||||
{
|
||||
"SimpleSerializable": {
|
||||
"type": {},
|
||||
"description": "any valid value"
|
||||
},
|
||||
"StringSerializable": {
|
||||
"type": "string"
|
||||
},
|
||||
"ListStringSerializable": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"BooleanSerializable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"NumberSerializable": {
|
||||
"type": "number"
|
||||
},
|
||||
"ImgSerializable": {
|
||||
"type": "string",
|
||||
"description": "base64 representation of an image"
|
||||
},
|
||||
"FileSerializable": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "filepath on your computer (or URL) of file"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "name of file" },
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "base64 representation of file"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"description": "size of image in bytes"
|
||||
},
|
||||
"is_file": {
|
||||
"type": "boolean",
|
||||
"description": "true if the file has been uploaded to the server"
|
||||
},
|
||||
"orig_name": {
|
||||
"type": "string",
|
||||
"description": "original name of the file"
|
||||
}
|
||||
},
|
||||
"required": ["name", "data"]
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "filepath on your computer (or URL) of file"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "name of file" },
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "base64 representation of file"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"description": "size of image in bytes"
|
||||
},
|
||||
"is_file": {
|
||||
"type": "boolean",
|
||||
"description": "true if the file has been uploaded to the server"
|
||||
},
|
||||
"orig_name": {
|
||||
"type": "string",
|
||||
"description": "original name of the file"
|
||||
}
|
||||
},
|
||||
"required": ["name", "data"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"SingleFileSerializable": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "filepath on your computer (or URL) of file"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "name of file" },
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "base64 representation of file"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"description": "size of image in bytes"
|
||||
},
|
||||
"is_file": {
|
||||
"type": "boolean",
|
||||
"description": "true if the file has been uploaded to the server"
|
||||
},
|
||||
"orig_name": {
|
||||
"type": "string",
|
||||
"description": "original name of the file"
|
||||
}
|
||||
},
|
||||
"required": ["name", "data"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"MultipleFileSerializable": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "filepath on your computer (or URL) of file"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "name of file" },
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "base64 representation of file"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"description": "size of image in bytes"
|
||||
},
|
||||
"is_file": {
|
||||
"type": "boolean",
|
||||
"description": "true if the file has been uploaded to the server"
|
||||
},
|
||||
"orig_name": {
|
||||
"type": "string",
|
||||
"description": "original name of the file"
|
||||
}
|
||||
},
|
||||
"required": ["name", "data"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"JSONSerializable": {
|
||||
"type": {},
|
||||
"description": "any valid json"
|
||||
},
|
||||
"GallerySerializable": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": false,
|
||||
"maxSize": 2,
|
||||
"minSize": 2,
|
||||
"prefixItems": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "name of file" },
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "base64 representation of file"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"description": "size of image in bytes"
|
||||
},
|
||||
"is_file": {
|
||||
"type": "boolean",
|
||||
"description": "true if the file has been uploaded to the server"
|
||||
},
|
||||
"orig_name": {
|
||||
"type": "string",
|
||||
"description": "original name of the file"
|
||||
}
|
||||
},
|
||||
"required": ["name", "data"]
|
||||
},
|
||||
{
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "caption of image" },
|
||||
{ "type": "null" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
[build-system]
|
||||
requires = ["hatchling", "hatch-requirements-txt", "hatch-fancy-pypi-readme>=22.5.0"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "gradio_client"
|
||||
dynamic = ["version", "dependencies", "readme"]
|
||||
description = "Python library for easily interacting with trained machine learning models"
|
||||
license = "Apache-2.0"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "Abubakar Abid", email = "gradio-team@huggingface.co" },
|
||||
{ name = "Ali Abid", email = "gradio-team@huggingface.co" },
|
||||
{ name = "Ali Abdalla", email = "gradio-team@huggingface.co" },
|
||||
{ name = "Dawood Khan", email = "gradio-team@huggingface.co" },
|
||||
{ name = "Ahsen Khaliq", email = "gradio-team@huggingface.co" },
|
||||
{ name = "Pete Allen", email = "gradio-team@huggingface.co" },
|
||||
{ name = "Freddy Boulton", email = "gradio-team@huggingface.co" },
|
||||
]
|
||||
keywords = ["machine learning", "client", "API"]
|
||||
|
||||
classifiers = [
|
||||
'Development Status :: 4 - Beta',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Topic :: Scientific/Engineering',
|
||||
'Topic :: Scientific/Engineering :: Artificial Intelligence',
|
||||
'Topic :: Software Development :: User Interfaces',
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/gradio-app/gradio"
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "gradio_client/package.json"
|
||||
pattern = ".*\"version\":\\s*\"(?P<version>[^\"]+)\""
|
||||
|
||||
[tool.hatch.metadata.hooks.requirements_txt]
|
||||
filename = "requirements.txt"
|
||||
|
||||
[tool.hatch.metadata.hooks.fancy-pypi-readme]
|
||||
content-type = "text/markdown"
|
||||
fragments = [
|
||||
{ path = "README.md" },
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = [
|
||||
"/gradio_client",
|
||||
"/README.md",
|
||||
"/requirements.txt",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = [
|
||||
"gradio_client"
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
GRADIO_ANALYTICS_ENABLED = "False"
|
||||
HF_HUB_DISABLE_TELEMETRY = "1"
|
||||
@@ -0,0 +1,5 @@
|
||||
fsspec
|
||||
httpx>=0.24.1
|
||||
huggingface_hub>=0.19.3,<2.0
|
||||
packaging
|
||||
typing_extensions~=4.0
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash -eu
|
||||
|
||||
cd "$(dirname ${0})/.."
|
||||
|
||||
echo "Testing..."
|
||||
python -m pytest test
|
||||
@@ -0,0 +1,515 @@
|
||||
import inspect
|
||||
import random
|
||||
import time
|
||||
|
||||
import gradio as gr
|
||||
import pytest
|
||||
from pydub import AudioSegment
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line(
|
||||
"markers", "flaky: mark test as flaky. Failure will not cause te"
|
||||
)
|
||||
config.addinivalue_line("markers", "serial: mark test as serial")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def calculator_demo():
|
||||
def calculator(num1, operation, num2):
|
||||
if operation == "add":
|
||||
return num1 + num2
|
||||
elif operation == "subtract":
|
||||
return num1 - num2
|
||||
elif operation == "multiply":
|
||||
return num1 * num2
|
||||
elif operation == "divide":
|
||||
if num2 == 0:
|
||||
raise gr.Error("Cannot divide by zero!")
|
||||
return num1 / num2
|
||||
|
||||
demo = gr.Interface(
|
||||
calculator,
|
||||
["number", gr.Radio(["add", "subtract", "multiply", "divide"]), "number"],
|
||||
"number",
|
||||
api_name="predict",
|
||||
examples=[
|
||||
[5, "add", 3],
|
||||
[4, "divide", 2],
|
||||
[-4, "multiply", 2.5],
|
||||
[0, "subtract", 1.2],
|
||||
],
|
||||
)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hello_world_demo():
|
||||
def greet(name, punctuation):
|
||||
return "Hello " + name + punctuation
|
||||
|
||||
demo = gr.Interface(
|
||||
fn=greet,
|
||||
inputs=[gr.Textbox(label="Name"), gr.Textbox(label="Punctuation")],
|
||||
outputs=gr.Textbox(label="Greeting"),
|
||||
api_name="greet",
|
||||
)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def calculator_demo_with_defaults():
|
||||
def calculator(num1, operation=None, num2=100):
|
||||
if operation is None or operation == "add":
|
||||
return num1 + num2
|
||||
elif operation == "subtract":
|
||||
return num1 - num2
|
||||
elif operation == "multiply":
|
||||
return num1 * num2
|
||||
elif operation == "divide":
|
||||
if num2 == 0:
|
||||
raise gr.Error("Cannot divide by zero!")
|
||||
return num1 / num2
|
||||
|
||||
demo = gr.Interface(
|
||||
calculator,
|
||||
[
|
||||
gr.Number(value=10),
|
||||
gr.Radio(["add", "subtract", "multiply", "divide"]),
|
||||
gr.Number(),
|
||||
],
|
||||
"number",
|
||||
examples=[
|
||||
[5, "add", 3],
|
||||
[4, "divide", 2],
|
||||
[-4, "multiply", 2.5],
|
||||
[0, "subtract", 1.2],
|
||||
],
|
||||
api_name="predict",
|
||||
)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def state_demo():
|
||||
state = gr.State(delete_callback=lambda x: print("STATE DELETED"))
|
||||
demo = gr.Interface(
|
||||
lambda x, y: (x, y),
|
||||
["textbox", state],
|
||||
["textbox", state],
|
||||
api_name="predict",
|
||||
)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def increment_demo():
|
||||
with gr.Blocks() as demo:
|
||||
btn1 = gr.Button("Increment")
|
||||
btn2 = gr.Button("Increment")
|
||||
btn3 = gr.Button("Increment")
|
||||
numb = gr.Number()
|
||||
|
||||
state = gr.State(0)
|
||||
|
||||
btn1.click(
|
||||
lambda x: (x + 1, x + 1),
|
||||
state,
|
||||
[state, numb],
|
||||
api_name="increment_with_queue",
|
||||
)
|
||||
btn2.click(
|
||||
lambda x: (x + 1, x + 1),
|
||||
state,
|
||||
[state, numb],
|
||||
queue=False,
|
||||
api_name="increment_without_queue",
|
||||
)
|
||||
btn3.click(
|
||||
lambda x: (x + 1, x + 1),
|
||||
state,
|
||||
[state, numb],
|
||||
api_visibility="private",
|
||||
)
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def progress_demo():
|
||||
def my_function(x, progress=gr.Progress()):
|
||||
progress(0, desc="Starting...")
|
||||
for _ in progress.tqdm(range(20)):
|
||||
time.sleep(0.1)
|
||||
return x
|
||||
|
||||
return gr.Interface(my_function, gr.Textbox(), gr.Textbox(), api_name="predict")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def yield_demo():
|
||||
def spell(x):
|
||||
for i in range(len(x)):
|
||||
time.sleep(0.5)
|
||||
yield x[:i]
|
||||
|
||||
return gr.Interface(spell, "textbox", "textbox", api_name="predict")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cancel_from_client_demo():
|
||||
def iteration():
|
||||
for i in range(20):
|
||||
print(f"i: {i}")
|
||||
yield i
|
||||
time.sleep(0.5)
|
||||
|
||||
def long_process():
|
||||
time.sleep(10)
|
||||
print("DONE!")
|
||||
return 10
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
num = gr.Number()
|
||||
|
||||
btn = gr.Button(value="Iterate")
|
||||
btn.click(iteration, None, num, api_name="iterate")
|
||||
btn2 = gr.Button(value="Long Process")
|
||||
btn2.click(long_process, None, num, api_name="long")
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sentiment_classification_demo():
|
||||
def classifier(text): # noqa: ARG001
|
||||
time.sleep(1)
|
||||
return {label: random.random() for label in ["POSITIVE", "NEGATIVE", "NEUTRAL"]}
|
||||
|
||||
def sleep_for_test():
|
||||
time.sleep(10)
|
||||
return 2
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
input_text = gr.Textbox(label="Input Text")
|
||||
with gr.Row():
|
||||
classify = gr.Button("Classify Sentiment")
|
||||
with gr.Column():
|
||||
label = gr.Label(label="Predicted Sentiment")
|
||||
number = gr.Number()
|
||||
btn = gr.Button("Sleep then print")
|
||||
classify.click(classifier, input_text, label, api_name="classify")
|
||||
btn.click(sleep_for_test, None, number, api_name="sleep")
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def count_generator_demo():
|
||||
def count(n):
|
||||
for i in range(int(n)):
|
||||
time.sleep(0.5)
|
||||
yield i
|
||||
|
||||
def show(n):
|
||||
return str(list(range(int(n))))
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Column():
|
||||
num = gr.Number(value=10)
|
||||
with gr.Row():
|
||||
count_btn = gr.Button("Count")
|
||||
list_btn = gr.Button("List")
|
||||
with gr.Column():
|
||||
out = gr.Textbox()
|
||||
|
||||
count_btn.click(count, num, out)
|
||||
list_btn.click(show, num, out)
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def count_generator_no_api():
|
||||
def count(n):
|
||||
for i in range(int(n)):
|
||||
time.sleep(0.5)
|
||||
yield i
|
||||
|
||||
def show(n):
|
||||
return str(list(range(int(n))))
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Column():
|
||||
num = gr.Number(value=10)
|
||||
with gr.Row():
|
||||
count_btn = gr.Button("Count")
|
||||
list_btn = gr.Button("List")
|
||||
with gr.Column():
|
||||
out = gr.Textbox()
|
||||
|
||||
count_btn.click(count, num, out, api_visibility="private")
|
||||
list_btn.click(show, num, out, api_visibility="private")
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def count_generator_demo_exception():
|
||||
def count(n):
|
||||
for i in range(int(n)):
|
||||
time.sleep(0.01)
|
||||
if i == 5:
|
||||
raise ValueError("Oh no!")
|
||||
yield i
|
||||
|
||||
def show(n):
|
||||
return str(list(range(int(n))))
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Column():
|
||||
num = gr.Number(value=10)
|
||||
with gr.Row():
|
||||
count_btn = gr.Button("Count")
|
||||
with gr.Column():
|
||||
out = gr.Textbox()
|
||||
|
||||
count_btn.click(count, num, out, api_name="count")
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def file_io_demo():
|
||||
demo = gr.Interface(
|
||||
lambda _: print("foox"),
|
||||
[gr.File(file_count="multiple"), "file"],
|
||||
[gr.File(file_count="multiple"), "file"],
|
||||
api_name="predict",
|
||||
)
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stateful_chatbot():
|
||||
with gr.Blocks() as demo:
|
||||
chatbot = gr.Chatbot()
|
||||
msg = gr.Textbox()
|
||||
clear = gr.Button("Clear")
|
||||
st = gr.State([1, 2, 3])
|
||||
|
||||
def respond(message, st, chat_history):
|
||||
assert st[0] == 1 and st[1] == 2 and st[2] == 3
|
||||
bot_message = "I love you"
|
||||
chat_history.append({"role": "user", "content": message})
|
||||
chat_history.append({"role": "assistant", "content": bot_message})
|
||||
return "", chat_history
|
||||
|
||||
msg.submit(respond, [msg, st, chatbot], [msg, chatbot], api_name="submit")
|
||||
clear.click(lambda: None, None, chatbot, queue=False)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hello_world_with_group():
|
||||
with gr.Blocks() as demo:
|
||||
name = gr.Textbox(label="name")
|
||||
output = gr.Textbox(label="greeting")
|
||||
greet = gr.Button("Greet")
|
||||
show_group = gr.Button("Show group")
|
||||
with gr.Group(visible=False) as group:
|
||||
gr.Textbox("Hello!")
|
||||
|
||||
def greeting(name):
|
||||
return f"Hello {name}", gr.Group(visible=True)
|
||||
|
||||
greet.click(
|
||||
greeting, inputs=[name], outputs=[output, group], api_name="greeting"
|
||||
)
|
||||
show_group.click(
|
||||
lambda: gr.Group(visible=False), None, group, api_name="show_group"
|
||||
)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hello_world_with_state_and_accordion():
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Row():
|
||||
name = gr.Textbox(label="name")
|
||||
output = gr.Textbox(label="greeting")
|
||||
num = gr.Number(label="count")
|
||||
with gr.Row():
|
||||
n_counts = gr.State(value=0)
|
||||
greet = gr.Button("Greet")
|
||||
open_acc = gr.Button("Open acc")
|
||||
close_acc = gr.Button("Close acc")
|
||||
with gr.Accordion(label="Extra stuff", open=False) as accordion:
|
||||
gr.Textbox("Hello!")
|
||||
|
||||
def greeting(name, state):
|
||||
"""This is a greeting function."""
|
||||
state += 1
|
||||
return state, f"Hello {name}", state, gr.Accordion(open=False)
|
||||
|
||||
greet.click(
|
||||
greeting,
|
||||
inputs=[name, n_counts],
|
||||
outputs=[n_counts, output, num, accordion],
|
||||
api_name="greeting",
|
||||
)
|
||||
open_acc.click(
|
||||
lambda state: (state + 1, state + 1, gr.Accordion(open=True)),
|
||||
[n_counts],
|
||||
[n_counts, num, accordion],
|
||||
api_name="open",
|
||||
)
|
||||
close_acc.click(
|
||||
lambda state: (state + 1, state + 1, gr.Accordion(open=False)),
|
||||
[n_counts],
|
||||
[n_counts, num, accordion],
|
||||
api_name="close",
|
||||
)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stream_audio():
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
def _stream_audio(audio_file):
|
||||
audio = AudioSegment.from_mp3(audio_file)
|
||||
i = 0
|
||||
chunk_size = 3000
|
||||
|
||||
while chunk_size * i < len(audio):
|
||||
chunk = audio[chunk_size * i : chunk_size * (i + 1)]
|
||||
i += 1
|
||||
if chunk:
|
||||
file = str(pathlib.Path(tempfile.gettempdir()) / f"{i}.wav")
|
||||
chunk.export(file, format="wav")
|
||||
yield file
|
||||
|
||||
return gr.Interface(
|
||||
fn=_stream_audio,
|
||||
inputs=gr.Audio(type="filepath", label="Audio file to stream"),
|
||||
outputs=gr.Audio(autoplay=True, streaming=True),
|
||||
api_name="predict",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def video_component():
|
||||
return gr.Interface(
|
||||
fn=lambda x: x, inputs=gr.Video(), outputs=gr.Video(), api_name="predict"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def all_components():
|
||||
classes_to_check = gr.components.Component.__subclasses__()
|
||||
subclasses = []
|
||||
|
||||
while classes_to_check:
|
||||
subclass = classes_to_check.pop()
|
||||
children = subclass.__subclasses__()
|
||||
|
||||
if children:
|
||||
classes_to_check.extend(children)
|
||||
if (
|
||||
"value" in inspect.signature(subclass).parameters
|
||||
and subclass != gr.components.Component
|
||||
and not getattr(subclass, "is_template", False)
|
||||
):
|
||||
subclasses.append(subclass)
|
||||
|
||||
return subclasses
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def gradio_temp_dir(monkeypatch, tmp_path):
|
||||
"""tmp_path is unique to each test function.
|
||||
It will be cleared automatically according to pytest docs: https://docs.pytest.org/en/6.2.x/reference.html#tmp-path
|
||||
"""
|
||||
monkeypatch.setenv("GRADIO_TEMP_DIR", str(tmp_path))
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def long_response_with_info():
|
||||
def long_response(_):
|
||||
gr.Info("Beginning long response")
|
||||
time.sleep(17)
|
||||
gr.Info("Done!")
|
||||
return "\ta\nb" * 90000
|
||||
|
||||
return gr.Interface(
|
||||
long_response, None, gr.Textbox(label="Output"), api_name="predict"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def many_endpoint_demo():
|
||||
with gr.Blocks() as demo:
|
||||
|
||||
def noop(x):
|
||||
return x
|
||||
|
||||
n_elements = 1000
|
||||
for _ in range(n_elements):
|
||||
msg2 = gr.Textbox()
|
||||
msg2.submit(noop, msg2, msg2)
|
||||
butn2 = gr.Button()
|
||||
butn2.click(noop, msg2, msg2)
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def max_file_size_demo():
|
||||
with gr.Blocks() as demo:
|
||||
file_1b = gr.File()
|
||||
upload_status = gr.Textbox()
|
||||
|
||||
file_1b.upload(
|
||||
lambda x: "Upload successful", file_1b, upload_status, api_name="upload_1b"
|
||||
)
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chatbot_message_format():
|
||||
with gr.Blocks() as demo:
|
||||
chatbot = gr.Chatbot()
|
||||
msg = gr.Textbox()
|
||||
|
||||
def respond(message, chat_history: list):
|
||||
bot_message = random.choice(
|
||||
["How are you?", "I love you", "I'm very hungry"]
|
||||
)
|
||||
chat_history.extend(
|
||||
[
|
||||
{"role": "user", "content": message},
|
||||
{"role": "assistant", "content": bot_message},
|
||||
]
|
||||
)
|
||||
return "", chat_history
|
||||
|
||||
msg.submit(respond, [msg, chatbot], [msg, chatbot], api_name="chat")
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def media_data():
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.append(Path(".").resolve().as_posix())
|
||||
from client.python.test import media_data
|
||||
|
||||
return media_data
|
||||
@@ -0,0 +1 @@
|
||||
abcdefghijklmnopqrstuvwxyz
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
pytest-asyncio
|
||||
pytest==7.1.2
|
||||
pytest-xdist
|
||||
ty==0.0.2
|
||||
pydub==0.25.1
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
from gradio_client import documentation
|
||||
|
||||
|
||||
class TestDocumentation:
|
||||
def test_website_documentation(self):
|
||||
docs = documentation.generate_documentation()
|
||||
assert len(docs) > 0
|
||||
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from contextlib import contextmanager
|
||||
|
||||
import gradio as gr
|
||||
|
||||
from gradio_client import Client
|
||||
from gradio_client.snippet import _stringify_py, generate_code_snippets
|
||||
|
||||
|
||||
@contextmanager
|
||||
def connect(demo: gr.Blocks, **kwargs):
|
||||
_, local_url, _ = demo.launch(prevent_thread_lock=True, **kwargs)
|
||||
try:
|
||||
yield Client(local_url)
|
||||
finally:
|
||||
demo.close()
|
||||
|
||||
|
||||
class TestSnippetExecution:
|
||||
def test_python_snippet_runs_for_simple_demo(self):
|
||||
def greet(name):
|
||||
return "Hello " + name + "!"
|
||||
|
||||
demo = gr.Interface(
|
||||
fn=greet,
|
||||
inputs=gr.Textbox(label="Name"),
|
||||
outputs=gr.Textbox(label="Greeting"),
|
||||
api_name="greet",
|
||||
)
|
||||
|
||||
with connect(demo) as client:
|
||||
api_info = client.view_api(print_info=False, return_format="dict")
|
||||
endpoint_info = api_info["named_endpoints"]["/greet"]
|
||||
snippets = generate_code_snippets("/greet", endpoint_info, client.src)
|
||||
|
||||
python_snippet = snippets["python"]
|
||||
assert "client.predict(" in python_snippet
|
||||
assert 'api_name="/greet"' in python_snippet
|
||||
|
||||
namespace = {}
|
||||
exec(python_snippet, namespace)
|
||||
assert namespace["result"] == "Hello Hello!!!"
|
||||
|
||||
def test_python_snippet_runs_for_calculator(self):
|
||||
def calculator(num1, operation, num2):
|
||||
if operation == "add":
|
||||
return num1 + num2
|
||||
elif operation == "subtract":
|
||||
return num1 - num2
|
||||
elif operation == "multiply":
|
||||
return num1 * num2
|
||||
elif operation == "divide":
|
||||
return num1 / num2
|
||||
|
||||
demo = gr.Interface(
|
||||
calculator,
|
||||
[
|
||||
"number",
|
||||
gr.Radio(["add", "subtract", "multiply", "divide"]),
|
||||
"number",
|
||||
],
|
||||
"number",
|
||||
api_name="predict",
|
||||
)
|
||||
|
||||
with connect(demo) as client:
|
||||
api_info = client.view_api(print_info=False, return_format="dict")
|
||||
endpoint_info = api_info["named_endpoints"]["/predict"]
|
||||
snippets = generate_code_snippets("/predict", endpoint_info, client.src)
|
||||
|
||||
python_snippet = snippets["python"]
|
||||
namespace = {}
|
||||
exec(python_snippet, namespace)
|
||||
assert namespace["result"] == 6.0
|
||||
|
||||
def test_python_snippet_runs_with_default_params(self):
|
||||
def add(a, b=10):
|
||||
return a + b
|
||||
|
||||
demo = gr.Interface(
|
||||
add,
|
||||
[gr.Number(label="a"), gr.Number(label="b", value=10)],
|
||||
gr.Number(label="result"),
|
||||
api_name="add",
|
||||
)
|
||||
|
||||
with connect(demo) as client:
|
||||
api_info = client.view_api(print_info=False, return_format="dict")
|
||||
endpoint_info = api_info["named_endpoints"]["/add"]
|
||||
snippets = generate_code_snippets("/add", endpoint_info, client.src)
|
||||
|
||||
python_snippet = snippets["python"]
|
||||
namespace = {}
|
||||
exec(python_snippet, namespace)
|
||||
assert isinstance(namespace["result"], (int, float))
|
||||
|
||||
|
||||
class TestStringifyPy:
|
||||
def test_datetime_in_nested_structure(self):
|
||||
"""Non-JSON-native types like datetime should not raise TypeError."""
|
||||
value = {
|
||||
"headers": ["t", "x"],
|
||||
"data": [[datetime.datetime(2026, 1, 1, 0, 0), 1]],
|
||||
}
|
||||
result = _stringify_py(value)
|
||||
assert "2026-01-01" in result
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_date_serialization(self):
|
||||
value = [datetime.date(2026, 6, 15)]
|
||||
result = _stringify_py(value)
|
||||
assert "2026-06-15" in result
|
||||
@@ -0,0 +1,334 @@
|
||||
import importlib.resources
|
||||
import json
|
||||
import tempfile
|
||||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal, Optional, Union
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from huggingface_hub import get_token
|
||||
|
||||
from gradio_client import utils
|
||||
|
||||
types = json.loads(importlib.resources.read_text("gradio_client", "types.json"))
|
||||
types["MultipleFile"] = {
|
||||
"type": "array",
|
||||
"items": {"type": "string", "description": "filepath or URL to file"},
|
||||
}
|
||||
types["SingleFile"] = {"type": "string", "description": "filepath or URL to file"}
|
||||
types["FileWithAdditionalProperties"] = {"type": "object", "additionalProperties": True}
|
||||
HF_TOKEN = get_token()
|
||||
|
||||
|
||||
class TestEnum(Enum):
|
||||
VALUE1 = "option1"
|
||||
VALUE2 = "option2"
|
||||
VALUE3 = 42
|
||||
|
||||
|
||||
def test_encode_url_or_file_to_base64(media_data):
|
||||
output_base64 = utils.encode_url_or_file_to_base64(
|
||||
Path(__file__).parents[3] / "gradio" / "test_data" / "test_image.png"
|
||||
)
|
||||
assert output_base64 == deepcopy(media_data.BASE64_IMAGE)
|
||||
|
||||
|
||||
def test_encode_file_to_base64(media_data):
|
||||
output_base64 = utils.encode_file_to_base64(
|
||||
Path(__file__).parents[3] / "gradio" / "test_data" / "test_image.png"
|
||||
)
|
||||
assert output_base64 == deepcopy(media_data.BASE64_IMAGE)
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
def test_encode_url_to_base64(media_data):
|
||||
output_base64 = utils.encode_url_to_base64(
|
||||
"https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/test_data/test_image.png"
|
||||
)
|
||||
assert output_base64 == deepcopy(media_data.BASE64_IMAGE)
|
||||
|
||||
|
||||
def test_encode_url_to_base64_doesnt_encode_errors(monkeypatch):
|
||||
request = httpx.Request("GET", "https://example.com/foo")
|
||||
error_response = httpx.Response(status_code=404, request=request)
|
||||
monkeypatch.setattr(httpx, "get", lambda *args, **kwargs: error_response)
|
||||
with pytest.raises(httpx.HTTPStatusError):
|
||||
utils.encode_url_to_base64("https://example.com/foo")
|
||||
|
||||
|
||||
def test_decode_base64_to_binary(media_data):
|
||||
binary = utils.decode_base64_to_binary(deepcopy(media_data.BASE64_IMAGE))
|
||||
assert deepcopy(media_data.BINARY_IMAGE) == binary
|
||||
|
||||
b64_img_without_header = deepcopy(media_data.BASE64_IMAGE).split(",")[1]
|
||||
binary_without_header, extension = utils.decode_base64_to_binary(
|
||||
b64_img_without_header
|
||||
)
|
||||
|
||||
assert binary[0] == binary_without_header
|
||||
assert extension is None
|
||||
|
||||
|
||||
def test_decode_base64_to_file(media_data):
|
||||
temp_file = utils.decode_base64_to_file(deepcopy(media_data.BASE64_IMAGE))
|
||||
assert isinstance(temp_file, tempfile._TemporaryFileWrapper)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path_or_url, file_types, expected_result",
|
||||
[
|
||||
("/home/user/documents/example.pdf", [".json", "text", ".mp3", ".pdf"], True),
|
||||
("C:\\Users\\user\\documents\\example.png", [".png"], True),
|
||||
("C:\\Users\\user\\documents\\example.png", ["image"], True),
|
||||
("C:\\Users\\user\\documents\\example.png", ["file"], True),
|
||||
("/home/user/documents/example.pdf", [".json", "text", ".mp3"], False),
|
||||
("https://example.com/avatar/xxxx.mp4", ["audio", ".png", ".jpg"], False),
|
||||
# WebP support - case insensitive
|
||||
("/home/user/images/photo.webp", ["image"], True),
|
||||
("/home/user/images/photo.WEBP", ["image"], True),
|
||||
("/home/user/images/photo.WebP", ["image"], True),
|
||||
("C:\\Users\\user\\images\\photo.webp", ["image", "video"], True),
|
||||
("C:\\Users\\user\\images\\photo.WEBP", ["image", "video"], True),
|
||||
],
|
||||
)
|
||||
def test_is_valid_file_type(path_or_url, file_types, expected_result):
|
||||
assert utils.is_valid_file(path_or_url, file_types) is expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filename, expected_mimetype",
|
||||
[
|
||||
("photo.webp", "image/webp"),
|
||||
("photo.WEBP", "image/webp"),
|
||||
("photo.WebP", "image/webp"),
|
||||
("video.vtt", "text/vtt"),
|
||||
("video.VTT", "text/vtt"),
|
||||
("image.png", "image/png"),
|
||||
],
|
||||
)
|
||||
def test_get_mimetype(filename, expected_mimetype):
|
||||
assert utils.get_mimetype(filename) == expected_mimetype
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"orig_filename, new_filename",
|
||||
[
|
||||
("abc", "abc"),
|
||||
("$$AAabc&3", "AAabc&3"),
|
||||
("$$AAa&..b-c3_", "AAa&..b-c3_"),
|
||||
("#.txt", "#.txt"),
|
||||
("###.pdf", "###.pdf"),
|
||||
("@!$.csv", "@.csv"),
|
||||
("a#.txt", "a#.txt"),
|
||||
# Path traversal characters are stripped
|
||||
("a/b\\c.txt", "abc.txt"),
|
||||
('a<b>c:"d.txt', "abcd.txt"),
|
||||
("a\x00b.txt", "ab.txt"),
|
||||
# Shell-dangerous characters ($, !, {, }) are stripped; parentheses and brackets preserved
|
||||
("[{(Hunting's Shadowsl!)}].epub", "[(Hunting's Shadowsl)].epub"),
|
||||
("l!)}]test[{(.txt", "l)]test[(.txt"),
|
||||
(
|
||||
"ゆかりです。私、こんなかわいい服は初めて着ました…。なんだかうれしくって、楽しいです。歌いたくなる気分って、初めてです。これがアイドルってことなのかもしれませんね",
|
||||
"ゆかりです。私、こんなかわいい服は初めて着ました…。なんだかうれしくって、楽しいです。歌いたくなる気分って、初めてです。これがアイト",
|
||||
),
|
||||
(
|
||||
"Bringing-computational-thinking-into-classrooms-a-systematic-review-on-supporting-teachers-in-integrating-computational-thinking-into-K12-classrooms_2024_Springer-Science-and-Business-Media-Deutschland-GmbH.pdf",
|
||||
"Bringing-computational-thinking-into-classrooms-a-systematic-review-on-supporting-teachers-in-integrating-computational-thinking-into-K12-classrooms_2024_Springer-Science-and-Business-Media-Deutsc.pdf",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_strip_invalid_filename_characters(orig_filename, new_filename):
|
||||
assert utils.strip_invalid_filename_characters(orig_filename) == new_filename
|
||||
|
||||
|
||||
class AsyncMock(MagicMock):
|
||||
async def __call__(self, *args, **kwargs):
|
||||
return super().__call__(*args, **kwargs)
|
||||
|
||||
|
||||
@patch("httpx.post")
|
||||
def test_sleep_successful(mock_post):
|
||||
utils.set_space_timeout("gradio/calculator")
|
||||
|
||||
|
||||
@patch(
|
||||
"httpx.post",
|
||||
side_effect=httpx.HTTPStatusError("error", request=None, response=None),
|
||||
)
|
||||
def test_sleep_unsuccessful(mock_post):
|
||||
with pytest.raises(utils.SpaceDuplicationError):
|
||||
utils.set_space_timeout("gradio/calculator")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("schema", types)
|
||||
def test_json_schema_to_python_type(schema):
|
||||
if schema == "SimpleSerializable":
|
||||
answer = "Any"
|
||||
elif schema == "StringSerializable":
|
||||
answer = "str"
|
||||
elif schema == "ListStringSerializable":
|
||||
answer = "list[str]"
|
||||
elif schema == "BooleanSerializable":
|
||||
answer = "bool"
|
||||
elif schema == "NumberSerializable":
|
||||
answer = "float"
|
||||
elif schema == "ImgSerializable":
|
||||
answer = "str"
|
||||
elif schema == "FileSerializable":
|
||||
answer = "str | dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file)) | list[str | dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file))]"
|
||||
elif schema == "JSONSerializable":
|
||||
answer = "str | float | bool | list | dict"
|
||||
elif schema == "GallerySerializable":
|
||||
answer = "tuple[dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file)), str | None]"
|
||||
elif schema == "SingleFileSerializable":
|
||||
answer = "str | dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file))"
|
||||
elif schema == "MultipleFileSerializable":
|
||||
answer = "list[str | dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file))]"
|
||||
elif schema == "SingleFile":
|
||||
answer = "str"
|
||||
elif schema == "MultipleFile":
|
||||
answer = "list[str]"
|
||||
elif schema == "FileWithAdditionalProperties":
|
||||
answer = "dict(str, Any)"
|
||||
else:
|
||||
raise ValueError(f"This test has not been modified to check {schema}")
|
||||
assert utils.json_schema_to_python_type(types[schema]) == answer
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"type_hint, expected_schema",
|
||||
[
|
||||
(str, {"type": "string"}),
|
||||
(int, {"type": "integer"}),
|
||||
(float, {"type": "number"}),
|
||||
(bool, {"type": "boolean"}),
|
||||
(type(None), {"type": "null"}),
|
||||
(Any, {}),
|
||||
(Union[str, int], {"anyOf": [{"type": "string"}, {"type": "integer"}]}),
|
||||
(Optional[str], {"oneOf": [{"type": "null"}, {"type": "string"}]}),
|
||||
(str | None, {"oneOf": [{"type": "null"}, {"type": "string"}]}),
|
||||
(dict, {"type": "object", "additionalProperties": {}}),
|
||||
(list, {"type": "array", "items": {}}),
|
||||
(tuple, {"type": "array"}),
|
||||
(set, {"type": "array", "uniqueItems": True}),
|
||||
(frozenset, {"type": "array", "uniqueItems": True}),
|
||||
(bytes, {"type": "string", "format": "byte"}),
|
||||
(bytearray, {"type": "string", "format": "byte"}),
|
||||
(TestEnum, {"enum": ["option1", "option2", 42]}),
|
||||
],
|
||||
)
|
||||
def test_python_type_to_json_schema(type_hint, expected_schema):
|
||||
schema = utils.python_type_to_json_schema(type_hint)
|
||||
assert schema == expected_schema
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"type_hint, expected_schema",
|
||||
[
|
||||
(tuple[int, ...], {"type": "array", "items": {"type": "integer"}}),
|
||||
(
|
||||
tuple[str, int],
|
||||
{
|
||||
"type": "array",
|
||||
"prefixItems": [{"type": "string"}, {"type": "integer"}],
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
},
|
||||
),
|
||||
(set[str], {"type": "array", "uniqueItems": True, "items": {"type": "string"}}),
|
||||
(
|
||||
list[str | None],
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"oneOf": [{"type": "null"}, {"type": "string"}]},
|
||||
},
|
||||
),
|
||||
(Literal["a", "b", "c"], {"enum": ["a", "b", "c"]}),
|
||||
(Literal["single"], {"const": "single"}),
|
||||
],
|
||||
)
|
||||
def test_python_type_to_json_schema_complex_nested_types(type_hint, expected_schema):
|
||||
assert utils.python_type_to_json_schema(type_hint) == expected_schema
|
||||
|
||||
|
||||
class TestConstructArgs:
|
||||
def test_no_parameters_empty_args(self):
|
||||
assert utils.construct_args(None, (), {}) == []
|
||||
|
||||
def test_no_parameters_with_args(self):
|
||||
assert utils.construct_args(None, (1, 2), {}) == [1, 2]
|
||||
|
||||
def test_no_parameters_with_kwargs(self):
|
||||
with pytest.raises(
|
||||
ValueError, match="This endpoint does not support key-word arguments"
|
||||
):
|
||||
utils.construct_args(None, (), {"a": 1})
|
||||
|
||||
def test_parameters_no_args_kwargs(self):
|
||||
parameters_info = [
|
||||
{
|
||||
"label": "param1",
|
||||
"parameter_name": "a",
|
||||
"parameter_has_default": True,
|
||||
"parameter_default": 10,
|
||||
}
|
||||
]
|
||||
assert utils.construct_args(parameters_info, (), {"a": 1}) == [1]
|
||||
|
||||
def test_parameters_with_args_no_kwargs(self):
|
||||
parameters_info = [{"label": "param1", "parameter_name": "a"}]
|
||||
assert utils.construct_args(parameters_info, (1,), {}) == [1]
|
||||
|
||||
def test_parameter_with_default_no_args_no_kwargs(self):
|
||||
parameters_info = [
|
||||
{"label": "param1", "parameter_has_default": True, "parameter_default": 10}
|
||||
]
|
||||
assert utils.construct_args(parameters_info, (), {}) == [10]
|
||||
|
||||
def test_args_filled_parameters_with_defaults(self):
|
||||
parameters_info = [
|
||||
{"label": "param1", "parameter_has_default": True, "parameter_default": 10},
|
||||
{"label": "param2", "parameter_has_default": True, "parameter_default": 20},
|
||||
]
|
||||
assert utils.construct_args(parameters_info, (1,), {}) == [1, 20]
|
||||
|
||||
def test_kwargs_filled_parameters_with_defaults(self):
|
||||
parameters_info = [
|
||||
{
|
||||
"label": "param1",
|
||||
"parameter_name": "a",
|
||||
"parameter_has_default": True,
|
||||
"parameter_default": 10,
|
||||
},
|
||||
{
|
||||
"label": "param2",
|
||||
"parameter_name": "b",
|
||||
"parameter_has_default": True,
|
||||
"parameter_default": 20,
|
||||
},
|
||||
]
|
||||
assert utils.construct_args(parameters_info, (), {"a": 1, "b": 2}) == [1, 2]
|
||||
|
||||
def test_positional_arg_and_kwarg_for_same_parameter(self):
|
||||
parameters_info = [{"label": "param1", "parameter_name": "a"}]
|
||||
with pytest.raises(
|
||||
TypeError, match="Parameter `a` is already set as a positional argument."
|
||||
):
|
||||
utils.construct_args(parameters_info, (1,), {"a": 2})
|
||||
|
||||
def test_invalid_kwarg(self):
|
||||
parameters_info = [{"label": "param1", "parameter_name": "a"}]
|
||||
with pytest.raises(
|
||||
TypeError, match="Parameter `b` is not a valid key-word argument."
|
||||
):
|
||||
utils.construct_args(parameters_info, (), {"b": 1})
|
||||
|
||||
def test_required_arg_missing(self):
|
||||
parameters_info = [{"label": "param1", "parameter_name": "a"}]
|
||||
with pytest.raises(
|
||||
TypeError, match="No value provided for required argument: a"
|
||||
):
|
||||
utils.construct_args(parameters_info, (), {})
|
||||
Reference in New Issue
Block a user