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):

Reply via email to