Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gradio custom component publish #6098

Merged
merged 15 commits into from
Oct 27, 2023
5 changes: 5 additions & 0 deletions .changeset/rich-grapes-slide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gradio": minor
---

feat:Gradio custom component publish
9 changes: 8 additions & 1 deletion gradio/cli/commands/components/_create_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ def _in_test_dir():
"""


PATTERN_RE = r"gradio-template-\w+"
PATTERN = "gradio-template-{template}"


@dataclasses.dataclass
class ComponentFiles:
template: str
Expand Down Expand Up @@ -299,7 +303,10 @@ def _create_backend(
pyproject = Path(__file__).parent / "files" / "pyproject_.toml"
pyproject_contents = pyproject.read_text()
pyproject_dest = directory / "pyproject.toml"
pyproject_dest.write_text(pyproject_contents.replace("<<name>>", package_name))
pyproject_contents = pyproject_contents.replace("<<name>>", package_name).replace(
"<<template>>", PATTERN.format(template=component.template)
)
pyproject_dest.write_text(pyproject_contents)

demo_dir = directory / "demo"
demo_dir.mkdir(exist_ok=True, parents=True)
Expand Down
2 changes: 2 additions & 0 deletions gradio/cli/commands/components/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .create import _create
from .dev import _dev
from .install_component import _install
from .publish import _publish
from .show import _show

app = Typer(help="Create and publish a new Gradio component")
Expand All @@ -18,3 +19,4 @@
app.command("install", help="Install the custom component in the current environment")(
_install
)
app.command("publish", help="Publish a component to PyPI and HuggingFace Hub")(_publish)
1 change: 1 addition & 0 deletions gradio/cli/commands/components/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def _build(
if pipe.returncode != 0:
live.update(":red_square: Build failed!")
live.update(pipe.stderr)
live.update(pipe.stdout)
return
else:
live.update(":white_check_mark: Build succeeded!")
Expand Down
11 changes: 11 additions & 0 deletions gradio/cli/commands/components/files/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
tags: [gradio-custom-component, gradio-custom-component-template-{{ template }}]
title: {theme_name}
Copy link
Member

@abidlabs abidlabs Oct 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo, {theme_name} should be presumably the name of the component

colorFrom: orange
colorTo: purple
sdk: gradio
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confused about this -- why is the sdk gradio if this is a Docker Space? I'm probably missing something

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file not the readme in the uploaded Space?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea sorry I need to delete this file! It's not used right now.

sdk_version: {gradio_version}
app_file: app.py
pinned: false
license: apache-2.0
---
1 change: 1 addition & 0 deletions gradio/cli/commands/components/files/pyproject_.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ keywords = [
"visualization",
"gradio",
"gradio custom component",
"<<template>>"
]
# Add dependencies here
dependencies = ["gradio"]
Expand Down
232 changes: 232 additions & 0 deletions gradio/cli/commands/components/publish.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import random
import re
import shutil
import tempfile
from pathlib import Path
from typing import Optional

from huggingface_hub import HfApi
from rich import print
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Confirm, Prompt
from typer import Argument, Option
from typing_extensions import Annotated

from gradio.cli.commands.components._create_utils import PATTERN_RE

colors = ["red", "yellow", "green", "blue", "indigo", "purple", "pink", "gray"]

PYPI_REGISTER_URL = "https://pypi.org/account/register/"

README_CONTENTS = """
---
tags: [gradio-custom-component{template}]
freddyaboulton marked this conversation as resolved.
Show resolved Hide resolved
title: {package_name} V{version}
colorFrom: {color_from}
colorTo: {color_to}
sdk: docker
pinned: false
license: apache-2.0
---
"""

DOCKERFILE = """
FROM python:3.9

WORKDIR /code

COPY . .

RUN pip install --no-cache-dir -r requirements.txt

ENV PYTHONUNBUFFERED=1 \
GRADIO_ALLOW_FLAGGING=never \
GRADIO_NUM_PORTS=1 \
GRADIO_SERVER_NAME=0.0.0.0 \
GRADIO_SERVER_PORT=7860 \
SYSTEM=spaces

CMD ["python", "app.py"]
"""


def _ignore(s, names):
ignored = []
for n in names:
if "__pycache__" in n or n.startswith("dist") or n.startswith("node_modules"):
ignored.append(n)
return ignored


def _publish(
wheel_file: Annotated[Path, Argument(help="Path to the wheel directory.")],
upload_pypi: Annotated[bool, Option(help="Whether to upload to PyPI.")] = True,
pypi_username: Annotated[str, Option(help="The username for PyPI.")] = "",
pypi_password: Annotated[str, Option(help="The password for PyPI.")] = "",
upload_demo: Annotated[
bool, Option(help="Whether to upload demo to HuggingFace.")
] = True,
demo_dir: Annotated[
Optional[Path], Option(help="Path to the demo directory.")
] = None,
source_dir: Annotated[
Optional[Path],
Option(
help="Path to the source directory of the custom component. To share with community."
),
] = None,
hf_token: Annotated[
Optional[str],
Option(
help="HuggingFace token for uploading demo. Can be omitted if already logged in via huggingface cli."
),
] = None,
):
upload_source = source_dir is not None
console = Console()
wheel_file = wheel_file.resolve()
if not wheel_file.suffix == ".whl":
raise ValueError(
"Please provide a wheel file. It must end with .whl. Run `gradio cc build` to create a wheel file."
)
if not wheel_file.exists():
raise ValueError(
f"{wheel_file} does not exist. Run `gradio cc build` to create a wheel file."
)

if upload_pypi and (not pypi_username or not pypi_password):
panel = Panel(
"It is recommended to upload your component to pypi so that [bold][magenta]anyone[/][/] "
"can install it with [bold][magenta]pip install[/][/].\n\n"
f"A PyPi account is needed. If you do not have an account, register account here: [blue]{PYPI_REGISTER_URL}[/]",
)
print(panel)
upload_pypi = Confirm.ask(":snake: Upload to pypi?")
if upload_pypi:
pypi_username = Prompt.ask(":laptop_computer: Enter your pypi username")
pypi_password = Prompt.ask(
":closed_lock_with_key: Enter your pypi password", password=True
)
if upload_pypi:
try:
from twine.commands.upload import upload as twine_upload # type: ignore
from twine.settings import Settings # type: ignore
except (ImportError, ModuleNotFoundError) as e:
raise ValueError(
"The twine library must be installed to publish to pypi."
"Install it with pip, pip install twine."
) from e

twine_settings = Settings(username=pypi_username, password=pypi_password)
try:
twine_upload(twine_settings, [str(wheel_file)])
except Exception:
console.print_exception()
if upload_demo and not demo_dir:
panel = Panel(
"It is recommended you upload a demo of your component to [blue]https://huggingface.co/spaces[/] "
"so that anyone can try it from their browser."
)
print(panel)
upload_demo = Confirm.ask(":hugging_face: Upload demo?")
if upload_demo:
panel = Panel(
"Please provide the path to the [magenta]demo directory[/] for your custom component.\n\n"
"This directory should contain [magenta]all the files[/] it needs to run successfully.\n\n"
"Please make sure the gradio app is in an [magenta]app.py[/] file.\n\n"
"If you need additional python requirements, add a [magenta]requirements.txt[/] file to this directory."
)
print(panel)
demo_dir = Path(
Prompt.ask(":roller_coaster: Please enter demo directory")
).resolve()
if upload_demo and not source_dir:
panel = Panel(
"It is recommended that you share your [magenta]source code[/] so that others can learn from and improve your component."
)
print(panel)
upload_source = Confirm.ask(":books: Would you like to share your source code?")
if upload_source:
source_dir = Path(
Prompt.ask(
":page_with_curl: Enter the path to the source code [magenta]directory[/] here"
)
).resolve()
if upload_demo:
assert demo_dir
if not (demo_dir / "app.py").exists():
raise FileNotFoundError("app.py not found in demo directory.")
additional_reqs = [
"https://gradio-builds.s3.amazonaws.com/4.0/attempt-05/gradio-4.0.0-py3-none-any.whl",
"https://gradio-builds.s3.amazonaws.com/4.0/attempt-05/gradio_client-0.7.0b0-py3-none-any.whl",
wheel_file.name,
]
if (demo_dir / "requirements.txt").exists():
reqs = (demo_dir / "requirements.txt").read_text().splitlines()
reqs += additional_reqs
else:
reqs = additional_reqs

color_from, color_to = random.choice(colors), random.choice(colors)
package_name, version = wheel_file.name.split("-")[:2]
with tempfile.TemporaryDirectory() as tempdir:
shutil.copytree(
str(demo_dir),
str(tempdir),
dirs_exist_ok=True,
)
if source_dir:
shutil.copytree(
str(source_dir),
str(Path(tempdir) / "src"),
dirs_exist_ok=True,
ignore=_ignore,
)
reqs_txt = Path(tempdir) / "requirements.txt"
reqs_txt.write_text("\n".join(reqs))
readme = Path(tempdir) / "README.md"
template = ""
if upload_source and source_dir:
match = re.search(
PATTERN_RE, (source_dir / "pyproject.toml").read_text()
)
if match:
template = f", {match.group(0)}"

readme.write_text(
README_CONTENTS.format(
package_name=package_name,
version=version,
color_from=color_from,
color_to=color_to,
template=template,
)
)
dockerfile = Path(tempdir) / "Dockerfile"
dockerfile.write_text(DOCKERFILE)

api = HfApi()
new_space = api.create_repo(
repo_id=f"{package_name}",
repo_type="space",
exist_ok=True,
private=False,
space_sdk="docker",
token=hf_token,
)
api.upload_folder(
repo_id=new_space.repo_id,
folder_path=tempdir,
token=hf_token,
repo_type="space",
)
api.upload_file(
repo_id=new_space.repo_id,
path_or_fileobj=str(wheel_file),
path_in_repo=wheel_file.name,
token=hf_token,
repo_type="space",
)
print("\n")
print(f"Demo uploaded to {new_space}!")
Loading