Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-huggingface-hub for
openSUSE:Factory checked in at 2026-09-07 11:30:09
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-huggingface-hub (Old)
and /work/SRC/openSUSE:Factory/.python-huggingface-hub.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-huggingface-hub"
Mon Sep 7 11:30:09 2026 rev:14 rq:1375750 version:1.30.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-huggingface-hub/python-huggingface-hub.changes
2026-08-28 19:48:11.787683864 +0200
+++
/work/SRC/openSUSE:Factory/.python-huggingface-hub.new.1265/python-huggingface-hub.changes
2026-09-07 11:31:49.070229759 +0200
@@ -1,0 +2,19 @@
+Fri Sep 4 07:59:02 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 1.30.0:
+ * hf jobs scheduled ls filters like hf jobs ls: --status
+ (active/suspended), repeatable --label key=value and --name;
+ -f/--filter is deprecated and now ignored with a warning
+ * HfApi.list_scheduled_jobs() takes a labels filter
+ * ResolvedRevision records the repo it was resolved against, so
+ reusing one on a different repo re-resolves instead of handing
+ back that repo's commit
+ * hf-inference serves chat completion for any text-generation or
+ image-text-to-text model, no conversational tag required
+ * Follow redirects between Hub hosts when resolving a file rather
+ than failing with a misleading connection error; CDN redirects
+ are still not followed, so Authorization never leaves the Hub
+ * Mark the Sandbox API experimental; shared sandboxes are meant
+ for workloads inside one trust boundary
+
+-------------------------------------------------------------------
Old:
----
huggingface_hub-1.29.0.tar.gz
New:
----
huggingface_hub-1.30.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-huggingface-hub.spec ++++++
--- /var/tmp/diff_new_pack.iZA6tr/_old 2026-09-07 11:31:49.949260575 +0200
+++ /var/tmp/diff_new_pack.iZA6tr/_new 2026-09-07 11:31:49.957260855 +0200
@@ -23,7 +23,7 @@
%endif
%{?sle15_python_module_pythons}
Name: python-huggingface-hub
-Version: 1.29.0
+Version: 1.30.0
Release: 0
Summary: Client library for interaction with the huggingface hub
License: Apache-2.0
++++++ huggingface_hub-1.29.0.tar.gz -> huggingface_hub-1.30.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/huggingface_hub-1.29.0/PKG-INFO
new/huggingface_hub-1.30.0/PKG-INFO
--- old/huggingface_hub-1.29.0/PKG-INFO 2026-08-27 14:18:21.244655400 +0200
+++ new/huggingface_hub-1.30.0/PKG-INFO 2026-09-03 12:04:59.705272000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: huggingface_hub
-Version: 1.29.0
+Version: 1.30.0
Summary: Client library to download and publish models, datasets and other
repos on the huggingface.co hub
Home-page: https://github.com/huggingface/huggingface_hub
Author: Hugging Face, Inc.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.29.0/src/huggingface_hub/__init__.py
new/huggingface_hub-1.30.0/src/huggingface_hub/__init__.py
--- old/huggingface_hub-1.29.0/src/huggingface_hub/__init__.py 2026-08-27
14:18:16.518984300 +0200
+++ new/huggingface_hub-1.30.0/src/huggingface_hub/__init__.py 2026-09-03
12:04:54.401985600 +0200
@@ -46,7 +46,7 @@
from typing import TYPE_CHECKING
-__version__ = "1.29.0"
+__version__ = "1.30.0"
# Alphabetical order of definitions is ensured in tests
# WARNING: any comment added in this dictionary definition will be lost when
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.29.0/src/huggingface_hub/_revision.py
new/huggingface_hub-1.30.0/src/huggingface_hub/_revision.py
--- old/huggingface_hub-1.29.0/src/huggingface_hub/_revision.py 2026-08-27
14:18:16.519732700 +0200
+++ new/huggingface_hub-1.30.0/src/huggingface_hub/_revision.py 2026-09-03
12:04:54.403204200 +0200
@@ -11,6 +11,10 @@
Instances are built by [`HfApi.resolve_revision`], which also caches the
`revision` -> `commit hash` mapping
in the local cache (`refs/` folder).
+ A commit hash only means something for the repo it was resolved against,
so an instance also remembers that
+ repo. Re-resolving it for another repo is not an error: the revision
initially requested is resolved again
+ (see [`HfApi.resolve_revision`]).
+
Attributes:
initial (`str` or `None`):
The revision initially requested by the user. If `None`, the
string value defaults to `"main"`.
@@ -32,16 +36,27 @@
initial: str | None
resolved: str
+ _repo_id: str | None
+ _repo_type: str
- def __new__(cls, resolved: str, initial: str | None = None) ->
"ResolvedRevision":
+ def __new__(
+ cls,
+ resolved: str,
+ initial: str | None = None,
+ repo_id: str | None = None,
+ repo_type: str | None = None,
+ ) -> "ResolvedRevision":
revision = super().__new__(cls, initial if initial is not None else
constants.DEFAULT_REVISION)
revision.initial = initial
revision.resolved = resolved
+ # The repo `resolved` belongs to. `None` means unknown, in which case
it is assumed to fit any repo.
+ revision._repo_id = repo_id
+ revision._repo_type = repo_type or constants.REPO_TYPE_MODEL
return revision
def __reduce__(self):
- # without this, pickle/copy rebuild the instance from its string value
only, losing `initial` and `resolved`
- return self.__class__, (self.resolved, self.initial)
+ # without this, pickle/copy rebuild the instance from its string value
only, losing the attributes
+ return self.__class__, (self.resolved, self.initial, self._repo_id,
self._repo_type)
def __repr__(self) -> str:
return f"ResolvedRevision(initial={self.initial!r},
resolved={self.resolved!r})"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.29.0/src/huggingface_hub/_sandbox.py
new/huggingface_hub-1.30.0/src/huggingface_hub/_sandbox.py
--- old/huggingface_hub-1.29.0/src/huggingface_hub/_sandbox.py 2026-08-27
14:18:16.519732700 +0200
+++ new/huggingface_hub-1.30.0/src/huggingface_hub/_sandbox.py 2026-09-03
12:04:54.403204200 +0200
@@ -473,6 +473,9 @@
class Sandbox:
"""An isolated cloud machine running on Hugging Face Jobs.
+ > [!NOTE]
+ > The Sandbox API is experimental. Its API and behavior may change without
notice.
+
Create a dedicated one with [`Sandbox.create`] (one job per sandbox), or
get many cheap shared ones from a [`SandboxPool`].
Reattach to a running sandbox from anywhere with [`Sandbox.connect`]. Use
as a context manager to terminate it on exit:
@@ -919,6 +922,10 @@
class SandboxPool:
"""A fleet of shared "host" jobs, each packing many landlock-isolated
sandboxes.
+ > [!NOTE]
+ > The Sandbox API is experimental. Its API and behavior may change without
notice. Shared sandboxes are intended
+ > for workloads within the same trust boundary; use [`Sandbox.create`] for
workloads that do not trust each other.
+
One host is one billed HF Job (a VM); it runs the sandbox server and
multiplexes
up to `sandboxes_per_host` lightweight sandboxes, isolated from each other
by
uid + the Landlock LSM. This makes large fan-outs cheap (the VM cost is
shared
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.29.0/src/huggingface_hub/cli/jobs.py
new/huggingface_hub-1.30.0/src/huggingface_hub/cli/jobs.py
--- old/huggingface_hub-1.29.0/src/huggingface_hub/cli/jobs.py 2026-08-27
14:18:16.521706600 +0200
+++ new/huggingface_hub-1.30.0/src/huggingface_hub/cli/jobs.py 2026-09-03
12:04:54.406204200 +0200
@@ -19,7 +19,7 @@
import shutil
import time
from collections.abc import Callable, Iterable
-from fnmatch import fnmatch
+from enum import Enum
from pathlib import Path
from queue import Empty, Queue
from typing import Annotated, Any, TypeVar
@@ -446,20 +446,6 @@
out.hint(f"Stream ended. Run `hf jobs inspect {job_ref}` to check the
final status (e.g. COMPLETED or ERROR).")
-def _matches_filters(job_properties: dict[str, str], filters: list[tuple[str,
str, str]]) -> bool:
- """Check if scheduled job matches all specified filters."""
- for key, op_str, pattern in filters:
- value = job_properties.get(key)
- if value is None:
- if op_str == "!=":
- continue
- return False
- match = fnmatch(value.lower(), pattern.lower())
- if (op_str == "=" and not match) or (op_str == "!=" and match):
- return False
- return True
-
-
def _clear_line(n: int) -> None:
LINE_UP = "\033[1A"
LINE_CLEAR = "\x1b[2K"
@@ -1000,6 +986,16 @@
_stream_logs_and_check_status(api, job)
+class ScheduledJobStatusFilter(str, Enum):
+ """Possible values for `hf jobs scheduled ls --status`.
+
+ Scheduled Jobs are not "running": they are either active (i.e. they will
trigger new runs) or suspended.
+ """
+
+ ACTIVE = "active"
+ SUSPENDED = "suspended"
+
+
scheduled_app = typer_factory(help="Create and manage scheduled Jobs on the
Hub.")
jobs_cli.add_group(scheduled_app, name="scheduled")
@@ -1060,16 +1056,54 @@
out.hint(f"Use `hf jobs scheduled inspect
{scheduled_job.owner.name}/{scheduled_job.id}` to view its details.")
-@scheduled_app.command("list | ls | ps", examples=["hf jobs scheduled ls"])
+@scheduled_app.command(
+ "list | ls | ps",
+ examples=[
+ "hf jobs scheduled ls",
+ "hf jobs scheduled ls -a",
+ "hf jobs scheduled ls --status suspended",
+ "hf jobs scheduled ls --name daily-script",
+ "hf jobs scheduled ls --label env=prod --label team=ml",
+ ],
+)
def scheduled_ps(
all: Annotated[
bool,
Option(
"-a",
"--all",
- help="Show all scheduled Jobs (default hides suspended)",
+ help="Show all scheduled Jobs (default hides suspended). Cannot be
combined with --status.",
),
] = False,
+ status: Annotated[
+ list[str] | None,
+ Option(
+ "--status",
+ click_type=SoftChoice(ScheduledJobStatusFilter),
+ help=(
+ "Only show scheduled Jobs with the given status.
Comma-separated or repeated, e.g."
+ " `--status suspended`."
+ ),
+ ),
+ ] = None,
+ label: Annotated[
+ list[str] | None,
+ Option(
+ "-l",
+ "--label",
+ help=(
+ "Only show scheduled Jobs with the given `key=value` label.
Repeat to require several labels, e.g."
+ " `--label env=prod --label team=ml`."
+ ),
+ ),
+ ] = None,
+ name: Annotated[
+ str | None,
+ Option(
+ "--name",
+ help="Only show scheduled Jobs with the given name (shortcut for
`--label name=NAME`).",
+ ),
+ ] = None,
namespace: NamespaceOpt = None,
token: TokenOpt = None,
filter: Annotated[
@@ -1077,45 +1111,67 @@
Option(
"-f",
"--filter",
- help="Filter output based on conditions provided (format:
key=value)",
+ help="(Deprecated) Use `--status` and `--label` instead.",
),
] = None,
) -> None:
- """List scheduled Jobs"""
+ """List scheduled Jobs.
+
+ Use `--status` to filter by status (`active` or `suspended`) and `--label`
to filter by `key=value` labels.
+ A scheduled Job must match every filter to be listed.
+ """
api = get_hf_api(token=token)
- scheduled_jobs = api.list_scheduled_jobs(namespace=namespace)
- filters: list[tuple[str, str, str]] = []
- for f in filter or []:
- if "=" in f:
- key, value = f.split("=", 1)
- # Negate predicate in case of key!=value
- if key.endswith("!"):
- op = "!="
- key = key[:-1]
- else:
- op = "="
- filters.append((key.lower(), op, value.lower()))
- else:
- out.warning(f"Ignoring invalid filter format '{f}'. Use key=value
format.")
- # Filter scheduled jobs (operating on ScheduledJobInfo objects to preserve
existing filter behavior)
+ if filter:
+ out.warning(
+ f"Ignoring filter '{filter}'."
+ " `-f`/`--filter` is deprecated and will be removed in a future
release. Use `--status`/`--label`."
+ )
+
+ if all and status:
+ raise CLIError("`-a`/`--all` cannot be combined with `--status`.")
+
+ # Status filtering (default to active scheduled Jobs, unless `--all` or
`--status` is provided).
+ raw_statuses: list[str] = []
+ for value in status or []:
+ raw_statuses.extend(part.strip().lower() for part in value.split(",")
if part.strip())
+
+ unknown_statuses = [s for s in raw_statuses if s not in
tuple(ScheduledJobStatusFilter)]
+ if unknown_statuses:
+ raise CLIError(
+ f"Invalid status filter(s) {unknown_statuses}: expected one of"
+ f" {[s.value for s in ScheduledJobStatusFilter]}."
+ )
+
+ if raw_statuses:
+ show_active = ScheduledJobStatusFilter.ACTIVE in raw_statuses
+ show_suspended = ScheduledJobStatusFilter.SUSPENDED in raw_statuses
+ else:
+ show_active = True
+ show_suspended = all
+
+ # Labels filtering
+ labels: dict[str, str] = {}
+ for raw_label in label or []:
+ if "=" not in raw_label:
+ raise CLIError(f"Invalid label filter '{raw_label}': must be in
the form 'key=value'")
+ key, value = raw_label.split("=", 1)
+ labels[key] = value
+
+ # `--name` is a shortcut for the `name` label.
+ if name is not None:
+ if "name" in labels:
+ raise CLIError("Cannot filter by both `--name` and `--label
name=...`.")
+ labels["name"] = name
+
+ scheduled_jobs = api.list_scheduled_jobs(namespace=namespace,
labels=labels or None)
+
filtered_jobs = []
for scheduled_job in scheduled_jobs:
suspend = scheduled_job.suspend or False
- if not all and suspend:
+ if suspend and not show_suspended:
continue
- image_or_space = scheduled_job.job_spec.docker_image or "N/A"
- cmd = scheduled_job.job_spec.command or []
- command_str = " ".join(cmd) if cmd else "N/A"
- job_name = (scheduled_job.job_spec.labels or {}).get("name") or "N/A"
- props = {
- "id": scheduled_job.id,
- "name": job_name,
- "image": image_or_space,
- "suspend": str(suspend),
- "command": command_str,
- }
- if not _matches_filters(props, filters):
+ if not suspend and not show_active:
continue
filtered_jobs.append(scheduled_job)
@@ -1142,9 +1198,14 @@
headers=["id", "name", "schedule", "image/space", "command",
"last_run", "next_run", "suspend"],
id_key="id",
)
- if not items and filters:
- filters_msg = ", ".join(f"{k}{o}{v}" for k, o, v in filters)
- out.text(f"No scheduled jobs matched filters: {filters_msg}")
+ if not items:
+ if raw_statuses or labels:
+ filters_msg = ", ".join(
+ [*(f"status={s}" for s in raw_statuses), *(f"label={k}={v}"
for k, v in labels.items())]
+ )
+ out.text(f"No scheduled jobs matched filters: {filters_msg}")
+ elif not all:
+ out.hint("No active scheduled jobs. Use `-a`/`--all` to include
suspended ones.")
if items:
first_item_id = items[0]["id"]
out.hint(f"Use `hf jobs scheduled inspect {first_item_id}` to view
details about a scheduled job.")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.29.0/src/huggingface_hub/cli/sandbox.py
new/huggingface_hub-1.30.0/src/huggingface_hub/cli/sandbox.py
--- old/huggingface_hub-1.29.0/src/huggingface_hub/cli/sandbox.py
2026-08-27 14:18:16.522706500 +0200
+++ new/huggingface_hub-1.30.0/src/huggingface_hub/cli/sandbox.py
2026-09-03 12:04:54.406204200 +0200
@@ -54,8 +54,10 @@
from .jobs import FlavorOpt, NamespaceOpt
-sandbox_cli = typer_factory(help="Run and manage sandboxes on Hugging Face
Jobs.")
-pool_cli = typer_factory(help="Warm pools of host VMs and spawn cheap shared
sandboxes from them.")
+sandbox_cli = typer_factory(help="Run and manage experimental sandboxes on
Hugging Face Jobs.")
+pool_cli = typer_factory(
+ help="Warm host VM pools and spawn experimental shared sandboxes for
workloads within the same trust boundary."
+)
sandbox_cli.add_group(pool_cli, name="pool")
process_cli = typer_factory(help="List and stop background processes running
in a sandbox.")
sandbox_cli.add_group(process_cli, name="process")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.29.0/src/huggingface_hub/constants.py
new/huggingface_hub-1.30.0/src/huggingface_hub/constants.py
--- old/huggingface_hub-1.29.0/src/huggingface_hub/constants.py 2026-08-27
14:18:16.522706500 +0200
+++ new/huggingface_hub-1.30.0/src/huggingface_hub/constants.py 2026-09-03
12:04:54.407204400 +0200
@@ -73,9 +73,9 @@
ENDPOINT = _HF_DEFAULT_STAGING_ENDPOINT
HUGGINGFACE_CO_URL_TEMPLATE = _HF_DEFAULT_STAGING_ENDPOINT +
"/{repo_id}/resolve/{revision}/{filename}"
-# Hosts whose web URLs can be parsed into a ``hf://`` URI (see
``huggingface_hub/utils/_hf_uris.py``).
-# Includes the public Hub host and its ``hf.co`` short domain, the staging
host, and the host of the
-# currently configured ``ENDPOINT`` so that self-hosted / staging endpoints
work too.
+# Hosts considered to be Hugging Face Hub endpoints: the public Hub host, its
``hf.co`` short domain, the staging
+# host, and the host of the configured ``ENDPOINT``. Used to parse web URLs
into ``hf://`` URIs and to decide which
+# redirects to follow when resolving files. The auth header is forwarded to
these hosts: only add trusted ones.
HF_URL_HOSTS: frozenset[str] = frozenset(
{"hf.co"}
| {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.29.0/src/huggingface_hub/file_download.py
new/huggingface_hub-1.30.0/src/huggingface_hub/file_download.py
--- old/huggingface_hub-1.29.0/src/huggingface_hub/file_download.py
2026-08-27 14:18:16.523706400 +0200
+++ new/huggingface_hub-1.30.0/src/huggingface_hub/file_download.py
2026-09-03 12:04:54.408204300 +0200
@@ -10,7 +10,7 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Any, BinaryIO, Literal, NoReturn, overload
-from urllib.parse import quote, urlparse
+from urllib.parse import quote
import httpx
from tqdm.auto import tqdm as base_tqdm
@@ -50,7 +50,8 @@
_DEFAULT_RETRY_ON_EXCEPTIONS,
_DEFAULT_RETRY_ON_STATUS_CODES,
_adjust_range_header,
- _httpx_follow_relative_redirects_with_backoff,
+ _httpx_follow_hub_redirects_with_backoff,
+ _is_same_or_hub_host,
http_stream_backoff,
)
from .utils._runtime import is_xet_available
@@ -1626,7 +1627,7 @@
hf_headers["Accept-Encoding"] = "identity" # prevent any compression =>
we want to know the real size of the file
# Retrieve metadata
- response = _httpx_follow_relative_redirects_with_backoff(
+ response = _httpx_follow_hub_redirects_with_backoff(
method="HEAD", url=url, headers=hf_headers, timeout=timeout,
retry_on_errors=retry_on_errors
)
hf_raise_for_status(response)
@@ -1748,9 +1749,10 @@
commit_hash = metadata.commit_hash
if commit_hash is None:
raise FileMetadataError(
- "Distant resource does not seem to be on huggingface.co.
It is possible that a configuration issue"
- " prevents you from downloading resources from
https://huggingface.co. Please check your firewall"
- " and proxy settings and make sure your SSL certificates
are updated."
+ f"Response from {url} is missing the
'{constants.HUGGINGFACE_HEADER_X_REPO_COMMIT}' header, so it"
+ " does not seem to be served by a Hugging Face Hub
endpoint. If HF_ENDPOINT is set, check that it"
+ " points to a Hub-compatible endpoint. Otherwise, check
your firewall and proxy settings and make"
+ " sure your SSL certificates are updated."
)
# Etag must exist
@@ -1772,12 +1774,12 @@
# and ensure we download the exact atomic version even if it
changed
# between the HEAD and the GET (unlikely, but hey).
#
- # If url domain is different => we are downloading from a CDN =>
url is signed => don't send auth
- # If url domain is the same => redirect due to repo rename AND
downloading a regular file => keep auth
+ # If the final location is on a Hub host (same host, or e.g.
huggingface.co reached through an
+ # HF_ENDPOINT mirror redirect) => keep auth. Otherwise it's a
signed CDN url => don't send auth.
if xet_file_data is None and url != metadata.location:
url_to_download = metadata.location
- if urlparse(url).netloc != urlparse(metadata.location).netloc:
- # Remove authorization header when downloading a LFS blob
+ if not _is_same_or_hub_host(url, metadata.location):
+ # Remove authorization header when downloading a LFS blob
from a CDN
headers.pop("authorization", None)
except httpx.ProxyError:
# Actually raise on proxy error
@@ -1911,6 +1913,11 @@
# Repo not found or gated => let's raise the actual error
# Unauthorized => likely a token issue => let's raise the actual error
raise head_call_error
+ elif isinstance(head_call_error, FileMetadataError):
+ # The call succeeded but the response lacked the metadata we need => a
configuration issue, not connectivity.
+ raise LocalEntryNotFoundError(
+ f"{head_call_error} We also cannot find the requested files in the
local cache."
+ ) from head_call_error
else:
# Otherwise: most likely a connection issue or Hub downtime => let's
warn the user
raise LocalEntryNotFoundError(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/huggingface_hub-1.29.0/src/huggingface_hub/hf_api.py
new/huggingface_hub-1.30.0/src/huggingface_hub/hf_api.py
--- old/huggingface_hub-1.29.0/src/huggingface_hub/hf_api.py 2026-08-27
14:18:16.525706500 +0200
+++ new/huggingface_hub-1.30.0/src/huggingface_hub/hf_api.py 2026-09-03
12:04:54.409204500 +0200
@@ -162,7 +162,7 @@
from .utils import tqdm as hf_tqdm
from .utils._auth import _get_token_from_environment, _get_token_from_file,
_get_token_from_google_colab
from .utils._deprecation import _deprecate_arguments, _deprecate_method
-from .utils._http import _httpx_follow_relative_redirects_with_backoff
+from .utils._http import _httpx_follow_hub_redirects_with_backoff
from .utils._runtime import is_xet_available
from .utils._typing import CallableT
from .utils._verification import collect_local_files, resolve_local_root,
verify_maps
@@ -3694,7 +3694,8 @@
`None` or `"model"` if it is a model. Default is `None`.
revision (`str`, *optional*):
The revision to resolve. Can be a branch name, a tag, a PR ref
or a commit hash. Defaults to the
- default branch. If a [`ResolvedRevision`] is passed, it is
returned as is.
+ default branch. If a [`ResolvedRevision`] is passed, it is
returned as is - unless it was resolved
+ against another repo, in which case the revision it initially
requested is resolved again.
cache_dir (`str`, `Path`, *optional*):
Path to the folder where cached files are stored. Defaults to
the value of `HF_HUB_CACHE`.
local_files_only (`bool`, *optional*, defaults to `False`):
@@ -3728,13 +3729,18 @@
>>> weights = hf_hub_download("openai-community/gpt2",
"model.safetensors", revision=revision)
```
"""
+ if repo_type is None:
+ repo_type = constants.REPO_TYPE_MODEL
+
if isinstance(revision, ResolvedRevision):
- return revision
+ # A commit hash means nothing outside of the repo it was resolved
for. `_repo_id=None` means the repo is
+ # unknown (instance built by hand), in which case it is assumed to
fit any repo.
+ if revision._repo_id is None or (revision._repo_id,
revision._repo_type) == (repo_id, repo_type):
+ return revision # already resolved for this repo => nothing
to do
+ revision = revision.initial # resolved for another repo =>
resolve what was initially requested
if revision is not None and REGEX_COMMIT_HASH.match(revision):
- return ResolvedRevision(resolved=revision, initial=revision)
+ return ResolvedRevision(resolved=revision, initial=revision,
repo_id=repo_id, repo_type=repo_type)
- if repo_type is None:
- repo_type = constants.REPO_TYPE_MODEL
if cache_dir is None:
cache_dir = constants.HF_HUB_CACHE
storage_folder = str(
@@ -3752,7 +3758,7 @@
)
except OSError as e:
logger.warning(f"Ignored error while caching commit hash
for '{repo_id}': {e}.")
- return ResolvedRevision(resolved=sha, initial=revision)
+ return ResolvedRevision(resolved=sha, initial=revision,
repo_id=repo_id, repo_type=repo_type)
except httpx.ProxyError:
# Actually raise on proxy error: a misconfigured proxy is not
an unreachable Hub
raise
@@ -3768,7 +3774,9 @@
if ref_path.is_file():
if error is not None:
logger.warning(f"Could not reach the Hub ({error}). Using
cached commit hash for '{repo_id}'.")
- return ResolvedRevision(resolved=ref_path.read_text().strip(),
initial=revision)
+ return ResolvedRevision(
+ resolved=ref_path.read_text().strip(), initial=revision,
repo_id=repo_id, repo_type=repo_type
+ )
reason = (
"'local_files_only=True' is set"
@@ -13053,6 +13061,7 @@
def list_scheduled_jobs(
self,
*,
+ labels: dict[str, str] | None = None,
timeout: int | None = None,
namespace: str | None = None,
token: bool | str | None = None,
@@ -13061,6 +13070,10 @@
List scheduled compute Jobs on Hugging Face infrastructure.
Args:
+ labels (`dict[str, str]`, *optional*):
+ Only return scheduled Jobs that have all the given `key=value`
labels, e.g.
+ `{"env": "prod", "team": "ml"}`.
+
timeout (`float`, *optional*):
Whether to set a timeout for the request to the Hub.
@@ -13071,16 +13084,33 @@
A valid user access token. If not provided, the locally saved
token will be used, which is the
recommended authentication method. Set to `False` to disable
authentication.
Refer to:
https://huggingface.co/docs/huggingface_hub/quick-start#authentication.
+
+ Returns:
+ `list[ScheduledJobInfo]`: a list of [`ScheduledJobInfo`] objects.
"""
if namespace is None:
namespace = self.whoami(token=token)["name"]
+ params: dict[str, Any] = {}
+ if labels:
+ # The endpoint only supports a single `label` filter server-side.
Send the first one to keep the
+ # payload small, then filter the remaining ones client-side.
+ key, value = next(iter(labels.items()))
+ params["label"] = f"{key}={value}"
response = get_session().get(
f"{self.endpoint}/api/scheduled-jobs/{namespace}",
headers=self._build_hf_headers(token=token),
+ params=params or None,
timeout=timeout,
)
hf_raise_for_status(response)
- return [ScheduledJobInfo(**scheduled_job_info) for scheduled_job_info
in response.json()]
+ scheduled_jobs = [ScheduledJobInfo(**scheduled_job_info) for
scheduled_job_info in response.json()]
+ if labels:
+ scheduled_jobs = [
+ scheduled_job
+ for scheduled_job in scheduled_jobs
+ if all((scheduled_job.job_spec.labels or {}).get(key) == value
for key, value in labels.items())
+ ]
+ return scheduled_jobs
def inspect_scheduled_job(
self,
@@ -14733,7 +14763,7 @@
42000
```
"""
- response = _httpx_follow_relative_redirects_with_backoff(
+ response = _httpx_follow_hub_redirects_with_backoff(
"HEAD",
f"{self.endpoint}/buckets/{bucket_id}/resolve/{quote(remote_path,
safe='')}",
headers=self._build_hf_headers(token=token),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.29.0/src/huggingface_hub/inference/_providers/hf_inference.py
new/huggingface_hub-1.30.0/src/huggingface_hub/inference/_providers/hf_inference.py
---
old/huggingface_hub-1.29.0/src/huggingface_hub/inference/_providers/hf_inference.py
2026-08-27 14:18:16.528803600 +0200
+++
new/huggingface_hub-1.30.0/src/huggingface_hub/inference/_providers/hf_inference.py
2026-09-03 12:04:54.413851700 +0200
@@ -176,16 +176,9 @@
model_info = HfApi().model_info(model)
pipeline_tag = model_info.pipeline_tag
tags = model_info.tags or []
- is_conversational = "conversational" in tags
if task in ("text-generation", "conversational"):
if pipeline_tag == "text-generation":
- # text-generation + conversational tag -> both tasks allowed
- if is_conversational:
- return
- # text-generation without conversational tag -> only
text-generation allowed
- if task == "text-generation":
- return
- raise ValueError(f"Model '{model}' doesn't support task '{task}'.")
+ return
if pipeline_tag == "text2text-generation":
if task == "text-generation":
@@ -193,8 +186,8 @@
raise ValueError(f"Model '{model}' doesn't support task '{task}'.")
if pipeline_tag == "image-text-to-text":
- if is_conversational and task == "conversational":
- return # Only conversational allowed if tagged as conversational
+ if task == "conversational":
+ return
raise ValueError("Non-conversational image-text-to-text task is not
supported.")
if (
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.29.0/src/huggingface_hub/utils/_http.py
new/huggingface_hub-1.30.0/src/huggingface_hub/utils/_http.py
--- old/huggingface_hub-1.29.0/src/huggingface_hub/utils/_http.py
2026-08-27 14:18:16.530706400 +0200
+++ new/huggingface_hub-1.30.0/src/huggingface_hub/utils/_http.py
2026-09-03 12:04:54.416204500 +0200
@@ -26,7 +26,7 @@
from dataclasses import dataclass
from shlex import quote
from typing import Any, TypeVar
-from urllib.parse import urlparse
+from urllib.parse import urljoin, urlparse
import httpx
@@ -691,14 +691,18 @@
)
-def _httpx_follow_relative_redirects_with_backoff(
+_MAX_REDIRECTS = 20 # same bound as httpx's default max_redirects
+
+
+def _httpx_follow_hub_redirects_with_backoff(
method: HTTP_METHOD_T, url: str, *, retry_on_errors: bool = False,
**httpx_kwargs
) -> httpx.Response:
- """Perform an HTTP request with backoff and follow relative redirects only.
+ """Perform an HTTP request with backoff, following redirects that stay on
the Hub.
Used to fetch HEAD /resolve on repo or bucket files.
- This is useful to follow a redirection to a renamed repository without
following redirection to a CDN.
+ Redirects to the same host or another Hub host are followed. Redirects to
any other host (CDN, storage
+ bucket) are not: the file metadata is on the redirect response itself and
the auth header must not leave the Hub.
A backoff mechanism retries the HTTP call on errors (429, 5xx, timeout,
network errors).
@@ -718,7 +722,7 @@
{} if retry_on_errors else {"retry_on_exceptions": (),
"retry_on_status_codes": ()}
)
- while True:
+ for _ in range(_MAX_REDIRECTS):
response = http_backoff(
method=method,
url=url,
@@ -728,18 +732,24 @@
)
hf_raise_for_status(response)
- # Check if response is a relative redirect
- if 300 <= response.status_code <= 399:
- parsed_target = urlparse(response.headers["Location"])
- if parsed_target.netloc == "":
- # Relative redirect -> update URL and retry
- url = urlparse(url)._replace(path=parsed_target.path).geturl()
- continue
+ if not response.has_redirect_location:
+ return response
+
+ target = urljoin(url, response.headers["Location"])
+ if not _is_same_or_hub_host(url, target):
+ # Redirect to a CDN or storage host (signed URL): the file
metadata is carried by this very response,
+ # and the authorization header must not be forwarded off the Hub
=> stop here.
+ return response
+
+ url = target
+
+ raise httpx.TooManyRedirects(f"Exceeded {_MAX_REDIRECTS} redirects while
resolving '{url}'.")
- # Break if no relative redirect
- break
- return response
+def _is_same_or_hub_host(url: str, target: str) -> bool:
+ """Whether `target` is served by the same host as `url`, or by a known Hub
host."""
+ target_host = (urlparse(target).hostname or "").lower()
+ return target_host == (urlparse(url).hostname or "").lower() or
target_host in constants.HF_URL_HOSTS
def fix_hf_endpoint_in_url(url: str, endpoint: str | None) -> str:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.29.0/src/huggingface_hub.egg-info/PKG-INFO
new/huggingface_hub-1.30.0/src/huggingface_hub.egg-info/PKG-INFO
--- old/huggingface_hub-1.29.0/src/huggingface_hub.egg-info/PKG-INFO
2026-08-27 14:18:21.186656000 +0200
+++ new/huggingface_hub-1.30.0/src/huggingface_hub.egg-info/PKG-INFO
2026-09-03 12:04:59.616270800 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: huggingface_hub
-Version: 1.29.0
+Version: 1.30.0
Summary: Client library to download and publish models, datasets and other
repos on the huggingface.co hub
Home-page: https://github.com/huggingface/huggingface_hub
Author: Hugging Face, Inc.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.29.0/tests/test_inference_providers.py
new/huggingface_hub-1.30.0/tests/test_inference_providers.py
--- old/huggingface_hub-1.29.0/tests/test_inference_providers.py
2026-08-27 14:18:16.535706300 +0200
+++ new/huggingface_hub-1.30.0/tests/test_inference_providers.py
2026-09-03 12:04:54.422204500 +0200
@@ -1134,7 +1134,7 @@
@pytest.mark.parametrize(
"pipeline_tag,tags,task,should_raise",
[
- # text-generation + no conversational tag -> only text-generation
allowed
+ # text-generation -> both tasks allowed, regardless of tags
(
"text-generation",
[],
@@ -1145,9 +1145,8 @@
"text-generation",
[],
"conversational",
- True,
+ False,
),
- # text-generation + conversational tag -> both tasks allowed
(
"text-generation",
["conversational"],
@@ -1160,7 +1159,7 @@
"conversational",
False,
),
- # image-text-to-text + conversational tag -> only conversational
allowed
+ # image-text-to-text -> only conversational allowed, regardless of
tags
(
"image-text-to-text",
["conversational"],
@@ -1177,7 +1176,7 @@
"image-text-to-text",
[],
"conversational",
- True,
+ False,
),
# text2text-generation only allowed for text-generation task
(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.29.0/tests/test_snapshot_download.py
new/huggingface_hub-1.30.0/tests/test_snapshot_download.py
--- old/huggingface_hub-1.29.0/tests/test_snapshot_download.py 2026-08-27
14:18:16.536706400 +0200
+++ new/huggingface_hub-1.30.0/tests/test_snapshot_download.py 2026-09-03
12:04:54.423204700 +0200
@@ -364,12 +364,15 @@
def test_revision_str_is_picklable():
- revision = ResolvedRevision(resolved=COMMIT_HASH, initial="refs/pr/4")
+ revision = ResolvedRevision(resolved=COMMIT_HASH, initial="refs/pr/4",
repo_id="user/repo", repo_type="dataset")
for restored in (pickle.loads(pickle.dumps(revision)),
copy.deepcopy(revision)):
assert restored == "refs/pr/4"
assert restored.initial == "refs/pr/4"
assert restored.resolved == COMMIT_HASH
+ # the repo it was resolved for survives as well, otherwise it would be
resolved again for that same repo
+ assert restored._repo_id == "user/repo"
+ assert restored._repo_type == "dataset"
class TestResolveRevision:
@@ -400,6 +403,29 @@
with offline():
assert api.resolve_revision(self.repo_id, revision=revision,
cache_dir=tmp_path) is revision
+ def test_resolve_revision_for_another_repo(self, api: HfApi, repo_factory,
tmp_path: Path):
+ """A revision resolved for one repo tells nothing about another one =>
it must be resolved again."""
+ revision = api.resolve_revision(self.repo_id, cache_dir=tmp_path)
+ other_repo_id = repo_factory().repo_id
+ other_commit_hash = api.create_commit(
+ repo_id=other_repo_id,
+ operations=[CommitOperationAdd(path_in_repo="dummy_file.txt",
path_or_fileobj=b"v1")],
+ commit_message="Add file to main branch",
+ ).oid
+
+ other_revision = api.resolve_revision(other_repo_id,
revision=revision, cache_dir=tmp_path)
+ assert other_revision == "main"
+ assert other_revision.resolved == other_commit_hash
+
+ # Another repo type is another repo as well
+ with pytest.raises(RepositoryNotFoundError):
+ api.resolve_revision(self.repo_id, repo_type="dataset",
revision=revision, cache_dir=tmp_path)
+
+ # Built by hand, without a repo => trusted for any repo
+ by_hand = ResolvedRevision(resolved=self.commit_hash)
+ with offline():
+ assert api.resolve_revision(other_repo_id, revision=by_hand,
cache_dir=tmp_path) is by_hand
+
def test_resolve_revision_not_cached(self, api: HfApi, tmp_path: Path):
with offline():
with pytest.raises(RevisionResolutionError):