Skip to content

Commit cc135ec

Browse files
[Jobs] Support commands in hf jobs uv run (#3303)
* uv run command * add tests * docs * Update src/huggingface_hub/hf_api.py Co-authored-by: célina <hanouticelina@gmail.com> * fix --------- Co-authored-by: célina <hanouticelina@gmail.com>
1 parent 1e2e204 commit cc135ec

File tree

4 files changed

+108
-9
lines changed

4 files changed

+108
-9
lines changed

docs/source/en/guides/cli.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,10 +753,16 @@ Run UV scripts (Python scripts with inline dependencies) on HF infrastructure:
753753
>>> hf jobs uv run ml_training.py --flavor gpu-t4-small
754754
755755
# Pass arguments to script
756-
>>> hf jobs uv run process.py input.csv output.parquet --repo data-scripts
756+
>>> hf jobs uv run process.py input.csv output.parquet
757+
758+
# Add dependencies
759+
>>> hf jobs uv run --with transformers --with torch train.py
757760
758761
# Run a script directly from a URL
759762
>>> hf jobs uv run https://huggingface.co/datasets/username/scripts/resolve/main/example.py
763+
764+
# Run a command
765+
>>> hf jobs uv run --with lighteval python -c "import lighteval"
760766
```
761767
762768
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/).

docs/source/en/guides/jobs.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,9 @@ Run UV scripts (Python scripts with inline dependencies) on HF infrastructure:
330330

331331
# Run a script directly from a URL
332332
>>> run_uv_job("https://huggingface.co/datasets/username/scripts/resolve/main/example.py")
333+
334+
# Run a command
335+
>>> run_uv_job("python", script_args=["-c", "import lighteval"], dependencies=["lighteval"])
333336
```
334337

335338
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/).

src/huggingface_hub/hf_api.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10297,10 +10297,10 @@ def run_uv_job(
1029710297
1029810298
Args:
1029910299
script (`str`):
10300-
Path or URL of the UV script.
10300+
Path or URL of the UV script, or a command.
1030110301
1030210302
script_args (`List[str]`, *optional*)
10303-
Arguments to pass to the script.
10303+
Arguments to pass to the script or command.
1030410304
1030510305
dependencies (`List[str]`, *optional*)
1030610306
Dependencies to use to run the UV script.
@@ -10335,10 +10335,31 @@ def run_uv_job(
1033510335
1033610336
Example:
1033710337
10338+
Run a script from a URL:
10339+
1033810340
```python
1033910341
>>> from huggingface_hub import run_uv_job
1034010342
>>> script = "https://raw.githubusercontent.com/huggingface/trl/refs/heads/main/trl/scripts/sft.py"
10341-
>>> run_uv_job(script, dependencies=["trl"], flavor="a10g-small")
10343+
>>> script_args = ["--model_name_or_path", "Qwen/Qwen2-0.5B", "--dataset_name", "trl-lib/Capybara", "--push_to_hub"]
10344+
>>> run_uv_job(script, script_args=script_args, dependencies=["trl"], flavor="a10g-small")
10345+
```
10346+
10347+
Run a local script:
10348+
10349+
```python
10350+
>>> from huggingface_hub import run_uv_job
10351+
>>> script = "my_sft.py"
10352+
>>> script_args = ["--model_name_or_path", "Qwen/Qwen2-0.5B", "--dataset_name", "trl-lib/Capybara", "--push_to_hub"]
10353+
>>> run_uv_job(script, script_args=script_args, dependencies=["trl"], flavor="a10g-small")
10354+
```
10355+
10356+
Run a command:
10357+
10358+
```python
10359+
>>> from huggingface_hub import run_uv_job
10360+
>>> script = "lighteval"
10361+
>>> script_args= ["endpoint", "inference-providers", "model_name=openai/gpt-oss-20b,provider=auto", "lighteval|gsm8k|0|0"]
10362+
>>> run_uv_job(script, script_args=script_args, dependencies=["lighteval"], flavor="a10g-small")
1034210363
```
1034310364
"""
1034410365
image = image or "ghcr.io/astral-sh/uv:python3.12-bookworm"
@@ -10357,8 +10378,9 @@ def run_uv_job(
1035710378
if namespace is None:
1035810379
namespace = self.whoami(token=token)["name"]
1035910380

10360-
if script.startswith("http://") or script.startswith("https://"):
10361-
# Direct URL execution - no upload needed
10381+
is_url = script.startswith("http://") or script.startswith("https://")
10382+
if is_url or not Path(script).is_file():
10383+
# Direct URL execution or command - no upload needed
1036210384
command = ["uv", "run"] + uv_args + [script] + script_args
1036310385
else:
1036410386
# Local file - upload to HF

tests/test_cli.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from huggingface_hub.cli.cache import CacheCommand
1111
from huggingface_hub.cli.download import DownloadCommand
12-
from huggingface_hub.cli.jobs import JobsCommands, RunCommand
12+
from huggingface_hub.cli.jobs import JobsCommands, RunCommand, UvCommand
1313
from huggingface_hub.cli.repo import RepoCommands
1414
from huggingface_hub.cli.repo_files import DeleteFilesSubCommand, RepoFilesCommand
1515
from huggingface_hub.cli.upload import UploadCommand
@@ -848,7 +848,7 @@ def setUp(self) -> None:
848848
commands_parser = self.parser.add_subparsers()
849849
JobsCommands.register_subcommand(commands_parser)
850850

851-
@patch(
851+
patch_requests_post = patch(
852852
"requests.Session.post",
853853
return_value=DummyResponse(
854854
{
@@ -862,7 +862,13 @@ def setUp(self) -> None:
862862
}
863863
),
864864
)
865-
@patch("huggingface_hub.hf_api.HfApi.whoami", return_value={"name": "my-username"})
865+
patch_whoami = patch("huggingface_hub.hf_api.HfApi.whoami", return_value={"name": "my-username"})
866+
patch_get_token = patch("huggingface_hub.hf_api.get_token", return_value="hf_xxx")
867+
patch_repo_info = patch("huggingface_hub.hf_api.HfApi.repo_info")
868+
patch_upload_file = patch("huggingface_hub.hf_api.HfApi.upload_file")
869+
870+
@patch_requests_post
871+
@patch_whoami
866872
def test_run(self, whoami: Mock, requests_post: Mock) -> None:
867873
input_args = ["jobs", "run", "--detach", "ubuntu", "echo", "hello"]
868874
cmd = RunCommand(self.parser.parse_args(input_args))
@@ -877,3 +883,65 @@ def test_run(self, whoami: Mock, requests_post: Mock) -> None:
877883
"flavor": "cpu-basic",
878884
"dockerImage": "ubuntu",
879885
}
886+
887+
@patch_requests_post
888+
@patch_whoami
889+
def test_uv_command(self, whoami: Mock, requests_post: Mock) -> None:
890+
input_args = ["jobs", "uv", "run", "--detach", "echo", "hello"]
891+
cmd = UvCommand(self.parser.parse_args(input_args))
892+
cmd.run()
893+
assert requests_post.call_count == 1
894+
args, kwargs = requests_post.call_args_list[0]
895+
assert args == ("https://huggingface.co/api/jobs/my-username",)
896+
assert kwargs["json"] == {
897+
"command": ["uv", "run", "echo", "hello"],
898+
"arguments": [],
899+
"environment": {},
900+
"flavor": "cpu-basic",
901+
"dockerImage": "ghcr.io/astral-sh/uv:python3.12-bookworm",
902+
}
903+
904+
@patch_requests_post
905+
@patch_whoami
906+
def test_uv_remote_script(self, whoami: Mock, requests_post: Mock) -> None:
907+
input_args = ["jobs", "uv", "run", "--detach", "https://.../script.py"]
908+
cmd = UvCommand(self.parser.parse_args(input_args))
909+
cmd.run()
910+
assert requests_post.call_count == 1
911+
args, kwargs = requests_post.call_args_list[0]
912+
assert args == ("https://huggingface.co/api/jobs/my-username",)
913+
assert kwargs["json"] == {
914+
"command": ["uv", "run", "https://.../script.py"],
915+
"arguments": [],
916+
"environment": {},
917+
"flavor": "cpu-basic",
918+
"dockerImage": "ghcr.io/astral-sh/uv:python3.12-bookworm",
919+
}
920+
921+
@patch_requests_post
922+
@patch_whoami
923+
@patch_get_token
924+
@patch_repo_info
925+
@patch_upload_file
926+
def test_uv_local_script(
927+
self, upload_file: Mock, repo_info: Mock, get_token: Mock, whoami: Mock, requests_post: Mock
928+
) -> None:
929+
input_args = ["jobs", "uv", "run", "--detach", __file__]
930+
cmd = UvCommand(self.parser.parse_args(input_args))
931+
cmd.run()
932+
assert requests_post.call_count == 1
933+
args, kwargs = requests_post.call_args_list[0]
934+
assert args == ("https://huggingface.co/api/jobs/my-username",)
935+
command = kwargs["json"].pop("command")
936+
assert "UV_SCRIPT_URL" in " ".join(command)
937+
assert kwargs["json"] == {
938+
"arguments": [],
939+
"environment": {
940+
"UV_SCRIPT_URL": "https://huggingface.co/datasets/my-username/hf-cli-jobs-uv-run-scripts/resolve/main/test_cli.py"
941+
},
942+
"secrets": {"UV_SCRIPT_HF_TOKEN": "hf_xxx"},
943+
"flavor": "cpu-basic",
944+
"dockerImage": "ghcr.io/astral-sh/uv:python3.12-bookworm",
945+
}
946+
assert repo_info.call_count == 1 # check if repo exists
947+
assert upload_file.call_count == 2 # script and readme

0 commit comments

Comments
 (0)