diff --git a/docs/source/en/guides/cli.md b/docs/source/en/guides/cli.md index e094f2953f..f789001eed 100644 --- a/docs/source/en/guides/cli.md +++ b/docs/source/en/guides/cli.md @@ -753,10 +753,16 @@ Run UV scripts (Python scripts with inline dependencies) on HF infrastructure: >>> hf jobs uv run ml_training.py --flavor gpu-t4-small # Pass arguments to script ->>> hf jobs uv run process.py input.csv output.parquet --repo data-scripts +>>> hf jobs uv run process.py input.csv output.parquet + +# Add dependencies +>>> hf jobs uv run --with transformers --with torch train.py # Run a script directly from a URL >>> hf jobs uv run https://huggingface.co/datasets/username/scripts/resolve/main/example.py + +# Run a command +>>> hf jobs uv run --with lighteval python -c "import lighteval" ``` UV scripts are Python scripts that include their dependencies directly in the file using a special comment syntax. This makes them perfect for self-contained tasks that don't require complex project setups. Learn more about UV scripts in the [UV documentation](https://docs.astral.sh/uv/guides/scripts/). diff --git a/docs/source/en/guides/jobs.md b/docs/source/en/guides/jobs.md index c9a339686b..5e5a66260a 100644 --- a/docs/source/en/guides/jobs.md +++ b/docs/source/en/guides/jobs.md @@ -324,6 +324,9 @@ Run UV scripts (Python scripts with inline dependencies) on HF infrastructure: # Run a script directly from a URL >>> run_uv_job("https://huggingface.co/datasets/username/scripts/resolve/main/example.py") + +# Run a command +>>> run_uv_job("python", script_args=["-c", "import lighteval"], dependencies=["lighteval"]) ``` UV scripts are Python scripts that include their dependencies directly in the file using a special comment syntax. This makes them perfect for self-contained tasks that don't require complex project setups. Learn more about UV scripts in the [UV documentation](https://docs.astral.sh/uv/guides/scripts/). diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index ec41ae26f2..a691275b63 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -10286,10 +10286,10 @@ def run_uv_job( Args: script (`str`): - Path or URL of the UV script. + Path or URL of the UV script, or a command. script_args (`List[str]`, *optional*) - Arguments to pass to the script. + Arguments to pass to the script or command. dependencies (`List[str]`, *optional*) Dependencies to use to run the UV script. @@ -10324,10 +10324,31 @@ def run_uv_job( Example: + Run a script from a URL: + ```python >>> from huggingface_hub import run_uv_job >>> script = "https://raw.githubusercontent.com/huggingface/trl/refs/heads/main/trl/scripts/sft.py" - >>> run_uv_job(script, dependencies=["trl"], flavor="a10g-small") + >>> script_args = ["--model_name_or_path", "Qwen/Qwen2-0.5B", "--dataset_name", "trl-lib/Capybara", "--push_to_hub"] + >>> run_uv_job(script, script_args=script_args, dependencies=["trl"], flavor="a10g-small") + ``` + + Run a local script: + + ```python + >>> from huggingface_hub import run_uv_job + >>> script = "my_sft.py" + >>> script_args = ["--model_name_or_path", "Qwen/Qwen2-0.5B", "--dataset_name", "trl-lib/Capybara", "--push_to_hub"] + >>> run_uv_job(script, script_args=script_args, dependencies=["trl"], flavor="a10g-small") + ``` + + Run a command: + + ```python + >>> from huggingface_hub import run_uv_job + >>> script = "lighteval" + >>> script_args= ["endpoint", "inference-providers", "model_name=openai/gpt-oss-20b,provider=auto", "lighteval|gsm8k|0|0"] + >>> run_uv_job(script, script_args=script_args, dependencies=["lighteval"], flavor="a10g-small") ``` """ image = image or "ghcr.io/astral-sh/uv:python3.12-bookworm" @@ -10346,8 +10367,8 @@ def run_uv_job( if namespace is None: namespace = self.whoami(token=token)["name"] - if script.startswith("http://") or script.startswith("https://"): - # Direct URL execution - no upload needed + if script.startswith("http://") or script.startswith("https://") or not script.endswith(".py"): + # Direct URL execution or command - no upload needed command = ["uv", "run"] + uv_args + [script] + script_args else: # Local file - upload to HF diff --git a/tests/test_cli.py b/tests/test_cli.py index 7fdf2fe4bd..ee6171a1e5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,7 +9,7 @@ from huggingface_hub.cli.cache import CacheCommand from huggingface_hub.cli.download import DownloadCommand -from huggingface_hub.cli.jobs import JobsCommands, RunCommand +from huggingface_hub.cli.jobs import JobsCommands, RunCommand, UvCommand from huggingface_hub.cli.repo import RepoCommands from huggingface_hub.cli.repo_files import DeleteFilesSubCommand, RepoFilesCommand from huggingface_hub.cli.upload import UploadCommand @@ -848,7 +848,7 @@ def setUp(self) -> None: commands_parser = self.parser.add_subparsers() JobsCommands.register_subcommand(commands_parser) - @patch( + patch_requests_post = patch( "requests.Session.post", return_value=DummyResponse( { @@ -862,7 +862,13 @@ def setUp(self) -> None: } ), ) - @patch("huggingface_hub.hf_api.HfApi.whoami", return_value={"name": "my-username"}) + patch_whoami = patch("huggingface_hub.hf_api.HfApi.whoami", return_value={"name": "my-username"}) + patch_get_token = patch("huggingface_hub.hf_api.get_token", return_value="hf_xxx") + patch_repo_info = patch("huggingface_hub.hf_api.HfApi.repo_info") + patch_upload_file = patch("huggingface_hub.hf_api.HfApi.upload_file") + + @patch_requests_post + @patch_whoami def test_run(self, whoami: Mock, requests_post: Mock) -> None: input_args = ["jobs", "run", "--detach", "ubuntu", "echo", "hello"] cmd = RunCommand(self.parser.parse_args(input_args)) @@ -877,3 +883,65 @@ def test_run(self, whoami: Mock, requests_post: Mock) -> None: "flavor": "cpu-basic", "dockerImage": "ubuntu", } + + @patch_requests_post + @patch_whoami + def test_uv_command(self, whoami: Mock, requests_post: Mock) -> None: + input_args = ["jobs", "uv", "run", "--detach", "echo", "hello"] + cmd = UvCommand(self.parser.parse_args(input_args)) + cmd.run() + assert requests_post.call_count == 1 + args, kwargs = requests_post.call_args_list[0] + assert args == ("https://huggingface.co/api/jobs/my-username",) + assert kwargs["json"] == { + "command": ["uv", "run", "echo", "hello"], + "arguments": [], + "environment": {}, + "flavor": "cpu-basic", + "dockerImage": "ghcr.io/astral-sh/uv:python3.12-bookworm", + } + + @patch_requests_post + @patch_whoami + def test_uv_remote_script(self, whoami: Mock, requests_post: Mock) -> None: + input_args = ["jobs", "uv", "run", "--detach", "https://.../script.py"] + cmd = UvCommand(self.parser.parse_args(input_args)) + cmd.run() + assert requests_post.call_count == 1 + args, kwargs = requests_post.call_args_list[0] + assert args == ("https://huggingface.co/api/jobs/my-username",) + assert kwargs["json"] == { + "command": ["uv", "run", "https://.../script.py"], + "arguments": [], + "environment": {}, + "flavor": "cpu-basic", + "dockerImage": "ghcr.io/astral-sh/uv:python3.12-bookworm", + } + + @patch_requests_post + @patch_whoami + @patch_get_token + @patch_repo_info + @patch_upload_file + def test_uv_local_script( + self, upload_file: Mock, repo_info: Mock, get_token: Mock, whoami: Mock, requests_post: Mock + ) -> None: + input_args = ["jobs", "uv", "run", "--detach", __file__] + cmd = UvCommand(self.parser.parse_args(input_args)) + cmd.run() + assert requests_post.call_count == 1 + args, kwargs = requests_post.call_args_list[0] + assert args == ("https://huggingface.co/api/jobs/my-username",) + command = kwargs["json"].pop("command") + assert "UV_SCRIPT_URL" in " ".join(command) + assert kwargs["json"] == { + "arguments": [], + "environment": { + "UV_SCRIPT_URL": "https://huggingface.co/datasets/my-username/hf-cli-jobs-uv-run-scripts/resolve/main/test_cli.py" + }, + "secrets": {"UV_SCRIPT_HF_TOKEN": "hf_xxx"}, + "flavor": "cpu-basic", + "dockerImage": "ghcr.io/astral-sh/uv:python3.12-bookworm", + } + assert repo_info.call_count == 1 # check if repo exists + assert upload_file.call_count == 2 # script and readme