Skip to content
42 changes: 42 additions & 0 deletions docs/source/en/guides/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -760,3 +760,45 @@ Run UV scripts (Python scripts with inline dependencies) on HF infrastructure:
```

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

### Scheduled Jobs

Schedule and manage jobs that will run on HF infrastructure.

The schedule should be one of `@annually`, `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`, or a CRON schedule expression (e.g., `"0 9 * * 1"` for 9 AM every Monday).

```bash
# Schedule a job that runs every hour
>>> hf jobs scheduled run @hourly python:3.12 python -c 'print("This runs every hour!")'

# Use the CRON syntax
>>> hf jobs scheduled run "*/5 * * * *" python:3.12 python -c 'print("This runs every 5 minutes!")'

# Schedule with GPU
>>> hf jobs schefuled run @hourly --flavor a10g-small pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel \
... python -c "import torch; print(f"This code ran with the following GPU: {torch.cuda.get_device_name()}")"

# Schedule a UV script
>>> hf jobs scheduled uv run @hourly my_script.py
```

Use the same parameters as `hf jobs run` to pass environment variables, secrets, timeout, etc.

Manage scheduled jobs using

```bash
# List your active scheduled jobs
>>> hf jobs scheduled ps

# Inspect the status of a job
>>> hf jobs scheduled inspect <scheduled_job_id>

# Suspend (pause) a scheduled job
>>> hf jobs scheduled suspend <scheduled_job_id>

# Resume a scheduled job
>>> hf jobs scheduled resume <scheduled_job_id>

# Delete a scheduled job
>>> hf jobs scheduled delete <scheduled_job_id>
```
61 changes: 61 additions & 0 deletions docs/source/en/guides/jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,64 @@ Run UV scripts (Python scripts with inline dependencies) on HF infrastructure:
```

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

### Scheduled Jobs

Schedule and manage jobs that will run on HF infrastructure.

Use [`schedule_job`] or [`schedule_uv_job`] with a schedule of `@annually`, `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`, or a CRON schedule expression (e.g., `"0 9 * * 1"` for 9 AM every Monday):

```python
# Schedule a job that runs every hour
>>> from huggingface_hub import schedule_job
>>> schedule_job(
... image="python:3.12",
... command=["python", "-c", "print('This runs every hour!')"],
... schedule="@hourly"
... )

# Use the CRON syntax
>>> schedule_job(
... image="python:3.12",
... command=["python", "-c", "print('This runs every 5 minutes!')"],
... schedule="*/5 * * * *"
... )

# Schedule with GPU
>>> schedule_job(
... image="pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel",
... command=["python", "-c", 'import torch; print(f"This code ran with the following GPU: {torch.cuda.get_device_name()}")'],
... schedule="@hourly",
... flavor="a10g-small",
... )

# Schedule a UV script
>>> from huggingface_hub import schedule_uv_job
>>> schedule_uv_job("my_script.py", schedule="@hourly")
```

Use the same parameters as [`run_job`] and [`run_uv_job`] to pass environment variables, secrets, timeout, etc.

Manage scheduled jobs using [`list_scheduled_jobs`], [`inspect_scheduled_job`], [`suspend_scheduled_job`], [`resume_scheduled_job`], and [`delete_scheduled_job`]:

```python
# List your active scheduled jobs
>>> from huggingface_hub import list_scheduled_jobs
>>> list_scheduled_jobs()

# Inspect the status of a job
>>> from huggingface_hub import inspect_scheduled_job
>>> inspect_scheduled_job(scheduled_job_id)

# Suspend (pause) a scheduled job
>>> from huggingface_hub import suspend_scheduled_job
>>> suspend_scheduled_job(scheduled_job_id)

# Resume a scheduled job
>>> from huggingface_hub import resume_scheduled_job
>>> resume_scheduled_job(scheduled_job_id)

# Delete a scheduled job
>>> from huggingface_hub import delete_scheduled_job
>>> delete_scheduled_job(scheduled_job_id)
```
18 changes: 18 additions & 0 deletions src/huggingface_hub/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@
"delete_folder",
"delete_inference_endpoint",
"delete_repo",
"delete_scheduled_job",
"delete_space_secret",
"delete_space_storage",
"delete_space_variable",
Expand Down Expand Up @@ -219,6 +220,7 @@
"get_webhook",
"grant_access",
"inspect_job",
"inspect_scheduled_job",
"list_accepted_access_requests",
"list_collections",
"list_datasets",
Expand Down Expand Up @@ -259,14 +261,18 @@
"request_space_storage",
"restart_space",
"resume_inference_endpoint",
"resume_scheduled_job",
"revision_exists",
"run_as_future",
"run_job",
"run_uv_job",
"scale_to_zero_inference_endpoint",
"schedule_job",
"schedule_uv_job",
"set_space_sleep_time",
"space_info",
"super_squash_history",
"suspend_scheduled_job",
"unlike",
"update_collection_item",
"update_collection_metadata",
Expand Down Expand Up @@ -838,6 +844,7 @@
"delete_folder",
"delete_inference_endpoint",
"delete_repo",
"delete_scheduled_job",
"delete_space_secret",
"delete_space_storage",
"delete_space_variable",
Expand Down Expand Up @@ -878,6 +885,7 @@
"hf_hub_download",
"hf_hub_url",
"inspect_job",
"inspect_scheduled_job",
"interpreter_login",
"list_accepted_access_requests",
"list_collections",
Expand Down Expand Up @@ -933,6 +941,7 @@
"request_space_storage",
"restart_space",
"resume_inference_endpoint",
"resume_scheduled_job",
"revision_exists",
"run_as_future",
"run_job",
Expand All @@ -942,13 +951,16 @@
"save_torch_state_dict",
"scale_to_zero_inference_endpoint",
"scan_cache_dir",
"schedule_job",
"schedule_uv_job",
"set_space_sleep_time",
"snapshot_download",
"space_info",
"split_state_dict_into_shards_factory",
"split_tf_state_dict_into_shards",
"split_torch_state_dict_into_shards",
"super_squash_history",
"suspend_scheduled_job",
"try_to_load_from_cache",
"unlike",
"update_collection_item",
Expand Down Expand Up @@ -1200,6 +1212,7 @@ def __dir__():
delete_folder, # noqa: F401
delete_inference_endpoint, # noqa: F401
delete_repo, # noqa: F401
delete_scheduled_job, # noqa: F401
delete_space_secret, # noqa: F401
delete_space_storage, # noqa: F401
delete_space_variable, # noqa: F401
Expand Down Expand Up @@ -1227,6 +1240,7 @@ def __dir__():
get_webhook, # noqa: F401
grant_access, # noqa: F401
inspect_job, # noqa: F401
inspect_scheduled_job, # noqa: F401
list_accepted_access_requests, # noqa: F401
list_collections, # noqa: F401
list_datasets, # noqa: F401
Expand Down Expand Up @@ -1267,14 +1281,18 @@ def __dir__():
request_space_storage, # noqa: F401
restart_space, # noqa: F401
resume_inference_endpoint, # noqa: F401
resume_scheduled_job, # noqa: F401
revision_exists, # noqa: F401
run_as_future, # noqa: F401
run_job, # noqa: F401
run_uv_job, # noqa: F401
scale_to_zero_inference_endpoint, # noqa: F401
schedule_job, # noqa: F401
schedule_uv_job, # noqa: F401
set_space_sleep_time, # noqa: F401
space_info, # noqa: F401
super_squash_history, # noqa: F401
suspend_scheduled_job, # noqa: F401
unlike, # noqa: F401
update_collection_item, # noqa: F401
update_collection_metadata, # noqa: F401
Expand Down
118 changes: 117 additions & 1 deletion src/huggingface_hub/_jobs_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class JobInfo:
status: (`JobStatus` or `None`):
Status of the Job, e.g. `JobStatus(stage="RUNNING", message=None)`
See [`JobStage`] for possible stage values.
status: (`JobOwner` or `None`):
owner: (`JobOwner` or `None`):
Owner of the Job, e.g. `JobOwner(id="5e9ecfc04957053f60648a3e", name="lhoestq", type="user")`

Example:
Expand Down Expand Up @@ -142,3 +142,119 @@ def __init__(self, **kwargs) -> None:
# Inferred fields
self.endpoint = kwargs.get("endpoint", constants.ENDPOINT)
self.url = f"{self.endpoint}/jobs/{self.owner.name}/{self.id}"


@dataclass
class JobSpec:
docker_image: Optional[str]
space_id: Optional[str]
command: Optional[List[str]]
arguments: Optional[List[str]]
environment: Optional[Dict[str, Any]]
secrets: Optional[Dict[str, Any]]
flavor: Optional[SpaceHardware]
timeout: Optional[int]
tags: Optional[List[str]]
arch: Optional[str]

def __init__(self, **kwargs) -> None:
self.docker_image = kwargs.get("dockerImage") or kwargs.get("docker_image")
self.space_id = kwargs.get("spaceId") or kwargs.get("space_id")
self.command = kwargs.get("command")
self.arguments = kwargs.get("arguments")
self.environment = kwargs.get("environment")
self.secrets = kwargs.get("secrets")
self.flavor = kwargs.get("flavor")
self.timeout = kwargs.get("timeout")
self.tags = kwargs.get("tags")
self.arch = kwargs.get("arch")


@dataclass
class LastJobInfo:
id: str
at: datetime

def __init__(self, **kwargs) -> None:
self.id = kwargs["id"]
self.at = parse_datetime(kwargs["at"])


@dataclass
class ScheduledJobStatus:
last_job: Optional[LastJobInfo]
next_job_run_at: datetime

def __init__(self, **kwargs) -> None:
last_job = kwargs.get("lastJob") or kwargs.get("last_job")
self.last_job = LastJobInfo(**last_job) if last_job else None
next_job_run_at = kwargs.get("nextJobRunAt") or kwargs.get("next_job_run_at")
self.next_job_run_at = parse_datetime(str(next_job_run_at))


@dataclass
class ScheduledJobInfo:
"""
Contains information about a Job.

Args:
id (`str`):
Scheduled Job ID.
created_at (`datetime` or `None`):
When the scheduled Job was created.
tags (`List[str]` or `None`):
The tags of the scheduled Job.
schedule (`str` or `None`):
One of "@annually", "@yearly", "@monthly", "@weekly", "@daily", "@hourly", or a
CRON schedule expression (e.g., '0 9 * * 1' for 9 AM every Monday).
suspend (`bool` or `None`):
Whether the the scheduled job is suspended (paused).
concurrency (`bool` or `None`):
Whether multiple instances of this Job can run concurrently.
status (`ScheduledJobStatus` or `None`):
Status of the scheduled Job.
owner: (`JobOwner` or `None`):
Owner of the scheduled Job, e.g. `JobOwner(id="5e9ecfc04957053f60648a3e", name="lhoestq", type="user")`
job_spec: (`JobSpec` or `None`):
Specifications of the Job.

Example:

```python
>>> from huggingface_hub import run_job
>>> scheduled_job = create_scheduled_job(
... image="python:3.12",
... command=["python", "-c", "print('Hello from the cloud!')"],
... schedule="@hourly",
... )
>>> scheduled_job.id
'687fb701029421ae5549d999'
>>> scheduled_job.status.next_job_run_at
datetime.datetime(2025, 7, 22, 17, 6, 25, 79000, tzinfo=datetime.timezone.utc)
```
"""

id: str
created_at: Optional[datetime]
job_spec: JobSpec
schedule: Optional[str]
suspend: Optional[bool]
concurrency: Optional[bool]
status: ScheduledJobStatus
owner: JobOwner

def __init__(self, **kwargs) -> None:
self.id = kwargs["id"]
created_at = kwargs.get("createdAt") or kwargs.get("created_at")
self.created_at = parse_datetime(created_at) if created_at else None
self.job_spec = JobSpec(**(kwargs.get("job_spec") or kwargs.get("jobSpec")))
self.schedule = kwargs.get("schedule")
self.suspend = kwargs.get("suspend")
self.concurrency = kwargs.get("concurrency")
status = kwargs.get("status", {})
self.status = ScheduledJobStatus(
last_job=status.get("last_job") or status.get("lastJob"),
next_job_run_at=status.get("next_job_run_at") or status.get("nextJobRunAt"),
)
owner = kwargs.get("owner", {})
self.owner = JobOwner(id=owner["id"], name=owner["name"], type=owner["type"])
Loading
Loading