Spaces:
Runtime error
Runtime error
import random | |
import re | |
import shutil | |
import tempfile | |
from pathlib import Path | |
from typing import List, Optional | |
import semantic_version | |
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 tomlkit import parse | |
from typer import Argument, Option | |
from typing_extensions import Annotated | |
colors = ["red", "yellow", "green", "blue", "indigo", "purple", "pink", "gray"] | |
PYPI_REGISTER_URL = "https://pypi.org/account/register/" | |
README_CONTENTS = """ | |
--- | |
tags: [gradio-custom-component{template}] | |
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 --link --chown=1000 . . | |
RUN mkdir -p /tmp/cache/ | |
RUN chmod a+rwx -R /tmp/cache/ | |
ENV TRANSFORMERS_CACHE=/tmp/cache/ | |
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 _get_version_from_file(dist_file: Path) -> Optional[str]: | |
match = re.search(r"-(\d+\.\d+\.\d+[a-zA-Z]*\d*)-", dist_file.name) | |
if match: | |
return match.group(1) | |
def _get_max_version(distribution_files: List[Path]) -> Optional[str]: | |
versions = [] | |
for p in distribution_files: | |
version = _get_version_from_file(p) | |
# If anything goes wrong, just return None so we upload all files | |
# better safe than sorry | |
if version: | |
try: | |
versions.append(semantic_version.Version(version)) | |
except ValueError: | |
return None | |
return str(max(versions)) if versions else None | |
def _publish( | |
dist_dir: Annotated[ | |
Path, | |
Argument(help=f"Path to the wheel directory. Default is {Path('.') / 'dist'}"), | |
] = Path(".") / "dist", | |
bump_version: Annotated[ | |
bool, Option(help="Whether to bump the version number.") | |
] = True, | |
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() | |
dist_dir = dist_dir.resolve() | |
name = None | |
description = None | |
if not dist_dir.exists(): | |
raise ValueError( | |
f"{dist_dir} does not exist. Run `gradio cc build` to create a wheel and source distribution." | |
) | |
if not dist_dir.is_dir(): | |
raise ValueError(f"{dist_dir} is not a directory") | |
distribution_files = [ | |
p.resolve() for p in Path(dist_dir).glob("*") if p.suffix in {".whl", ".gz"} | |
] | |
wheel_file = next((p for p in distribution_files if p.suffix == ".whl"), None) | |
if not wheel_file: | |
raise ValueError( | |
"A wheel file was not found in the distribution directory. " | |
"Run `gradio cc build` to create a wheel file." | |
) | |
config_file = None | |
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 and (Path.home() / ".pypirc").exists(): | |
print(":closed_lock_with_key: Found .pypirc file in home directory.") | |
config_file = str(Path.home() / ".pypirc") | |
elif upload_pypi: | |
print( | |
":light_bulb: If you have Two Factor Authentication enabled, the username is __token__ and your password is your API key." | |
) | |
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 | |
if pypi_username and pypi_password: | |
twine_settings = Settings(username=pypi_username, password=pypi_password) | |
elif config_file: | |
twine_settings = Settings(config_file=config_file) | |
else: | |
raise ValueError( | |
"No pypi username or password provided and no ~/.pypirc file found." | |
) | |
try: | |
# do our best to only upload the latest versions | |
max_version = _get_max_version(distribution_files) | |
if not max_version: | |
# Have to write it in this awkward way cause ruff doesn't like lambdas | |
# and black doesn't like two functions with the same name | |
def predicate_(p): | |
return True | |
twine_files = [str(p) for p in distribution_files if predicate_(p)] | |
else: | |
def predicate(p): | |
return max_version in p.name | |
twine_files = [str(p) for p in distribution_files if predicate(p)] | |
print(f"Uploading files: {','.join(twine_files)}") | |
twine_upload(twine_settings, twine_files) | |
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_ = Prompt.ask( | |
f":roller_coaster: Please enter the path to the demo directory. Leave blank to use: {(Path('.') / 'demo')}" | |
) | |
demo_dir_ = demo_dir_ or str(Path(".") / "demo") | |
demo_dir = Path(demo_dir_).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_ = Prompt.ask( | |
":page_with_curl: Enter the path to the source code [magenta]directory[/]. Leave blank to use current directory" | |
) | |
source_dir_ = source_dir_ or str(Path(".")) | |
source_dir = Path(source_dir_).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 = [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: | |
pyproject_toml = parse((source_dir / "pyproject.toml").read_text()) | |
keywords = pyproject_toml["project"].get("keywords", []) # type: ignore | |
keywords = [ | |
k | |
for k in keywords | |
if k not in {"gradio-custom-component", "gradio custom component"} | |
] | |
if keywords: | |
template = "," + ",".join(keywords) | |
name = pyproject_toml["project"]["name"] # type: ignore | |
description = pyproject_toml["project"]["description"] # type: ignore | |
readme_text = README_CONTENTS.format( | |
package_name=package_name, | |
version=version, | |
color_from=color_from, | |
color_to=color_to, | |
template=template, | |
) | |
if name and description: | |
readme_text += f"\n\n# Name: {name}" | |
readme_text += f"\n\nDescription: {description}" | |
readme_text += f"\n\nInstall with: pip install {package_name}" | |
readme.write_text(readme_text) | |
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} !") | |