From c904c9af994c9de14c3e4f7c143bb07d3f932d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E6=B3=93?= Date: Wed, 5 Aug 2026 17:22:16 +0800 Subject: [PATCH 1/4] change visibility api --- src/modelscope_hub/agent/__init__.py | 11 +++- src/modelscope_hub/agent/_api.py | 89 +++++++++++++++++++++++++++- src/modelscope_hub/cli/agent.py | 7 ++- 3 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/modelscope_hub/agent/__init__.py b/src/modelscope_hub/agent/__init__.py index 81d0cd9..f07c50b 100644 --- a/src/modelscope_hub/agent/__init__.py +++ b/src/modelscope_hub/agent/__init__.py @@ -11,11 +11,20 @@ (download/commit/LFS/list/create/delete). - :class:`RemoteFileInfo` -- metadata for a single remote file. - :func:`is_lfs_file` -- decide whether a file must use the LFS upload path. +- ``agent_visibility_label`` / ``agent_last_modified`` / ``agent_downloads`` / + ``agent_logo_url`` -- read renamed agent metadata fields from an API item, + tolerating both JSON spellings (snake_case and PascalCase) and legacy keys. """ -from ._api import AgentApi, RemoteFileInfo, is_lfs_file +from ._api import (AgentApi, RemoteFileInfo, agent_downloads, + agent_last_modified, agent_logo_url, + agent_visibility_label, is_lfs_file) __all__ = [ "AgentApi", "RemoteFileInfo", "is_lfs_file", + "agent_visibility_label", + "agent_last_modified", + "agent_downloads", + "agent_logo_url", ] diff --git a/src/modelscope_hub/agent/_api.py b/src/modelscope_hub/agent/_api.py index 32c0965..3c57625 100644 --- a/src/modelscope_hub/agent/_api.py +++ b/src/modelscope_hub/agent/_api.py @@ -45,6 +45,81 @@ _LFS_SIZE_THRESHOLD: int = 1 * 1024 * 1024 # 1 MB +def agent_visibility_label(item: dict) -> str: + """Read an agent's visibility from an API item as a public/private label. + + The agent API replaced the ``visibility`` string with a boolean ``private`` + of INVERTED meaning (``private=false`` is public), in both snake_case + (OpenAPI / detail) and PascalCase (list / search) spellings. Reading the + raw field directly is a trap: ``False`` is falsy, so an ``or``-chain would + silently report a public agent as unknown. Legacy ``visibility`` keys are + still honoured so this works against older servers. + """ + # Current field: a plain bool, so truthiness is the whole story -- no + # casing/whitespace normalization applies. ``key in item`` (not ``or``) + # because ``private=False`` means PUBLIC and would be skipped as falsy. + for key in ("Private", "private"): + if key in item and item[key] is not None: + return (Visibility.PRIVATE.label + if item[key] else Visibility.PUBLIC.label) + + # Legacy field, which arrived in several shapes: a label of any casing + # (``"Public"``), an int enum (1/3/5) or its numeric string -- hence the + # normalization below. Unknown values are echoed rather than guessed: + # the old server logic treated everything != "public" as private, which + # is exactly how ``"Public"`` used to flip an agent private by accident. + raw = item.get("Visibility") + if raw is None: + raw = item.get("visibility") + if raw is None or raw == "": + return "-" + if isinstance(raw, bool): # bool is an int subclass -- check it first + return Visibility.PRIVATE.label if raw else Visibility.PUBLIC.label + try: + if isinstance(raw, int): + return Visibility(raw).label + return Visibility.from_label(str(raw).strip().lower()).label + except (ValueError, KeyError): + return str(raw).strip().lower() or "-" + + +def agent_last_modified(item: dict) -> str: + """Read an agent's last-modified timestamp from an API item. + + ``last_modified`` / ``LastModified`` superseded ``gmt_modified`` and is now + UTC RFC3339, so it must not be shown as if it were local time. Older keys + are accepted as a fallback. + """ + for key in ("LastModified", "last_modified", "GmtModified", "gmt_modified", + "LastUpdatedDate", "last_updated_date"): + val = item.get(key) + if val: + return str(val) + return "-" + + +def agent_downloads(item: dict) -> int: + """Read an agent's download count (``downloads``, was ``download_count``). + """ + for key in ("Downloads", "downloads", "DownloadCount", "download_count"): + val = item.get(key) + if val is not None: + try: + return int(val) + except (TypeError, ValueError): + return 0 + return 0 + + +def agent_logo_url(item: dict) -> str: + """Read an agent's logo URL (``logo_url``, formerly ``logo``).""" + for key in ("LogoUrl", "logo_url", "Logo", "logo"): + val = item.get(key) + if val: + return str(val) + return "" + + @dataclass class RemoteFileInfo: """Metadata for a single file in the remote repository.""" @@ -192,13 +267,23 @@ def create_repo(self, path: str, name: str, framework: str | None = None, repo (e.g. "qoder", "nanobot"). Defaults to server-side default when omitted. visibility: Repository visibility, ``"public"`` (default) or - ``"private"``. + ``"private"``. Kept as a label for a stable caller- + facing API; it is sent over the wire as the boolean + ``private`` field (see below). """ allowed = (Visibility.PUBLIC.label, Visibility.PRIVATE.label) if visibility not in allowed: raise ValueError( f"visibility must be one of {allowed}, got {visibility!r}") - body: dict = {"path": path, "name": name, "visibility": visibility} + # The agent API takes a boolean ``private`` (INVERTED semantics), not + # the old ``visibility`` string. A string here would be rejected with + # 400, and omitting it would silently default to public, so always + # send an explicit bool. + body: dict = { + "path": path, + "name": name, + "private": visibility == Visibility.PRIVATE.label, + } if framework: body["framework"] = framework return self._openapi.request("POST", "/agents", json_body=body) diff --git a/src/modelscope_hub/cli/agent.py b/src/modelscope_hub/cli/agent.py index 5454da3..7793c42 100644 --- a/src/modelscope_hub/cli/agent.py +++ b/src/modelscope_hub/cli/agent.py @@ -14,7 +14,8 @@ from argparse import Action, RawDescriptionHelpFormatter from pathlib import Path -from ..agent import AgentApi, is_lfs_file +from ..agent import (AgentApi, agent_last_modified, agent_visibility_label, + is_lfs_file) from ..constants import Visibility from ..errors import APIError from .base import CLICommand @@ -88,8 +89,8 @@ def _cmd_list(owner, page_number, page_size, *, endpoint, token) -> int: name = item.get("Name") or item.get("name") or "" repo_id = f"{owner_name}/{name}" if owner_name else name fw = item.get("Framework") or item.get("framework") or "-" - vis = item.get("Visibility") or item.get("visibility") or "-" - updated = item.get("LastUpdatedDate") or item.get("last_updated_date") or "-" + vis = agent_visibility_label(item) + updated = agent_last_modified(item) if isinstance(updated, str) and "T" in updated: updated = updated.split("T")[0] rows.append((repo_id, fw, vis, updated)) From 8dacda2c443813ecf2553c1c39c8f3466f9cfec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E6=B3=93?= Date: Wed, 5 Aug 2026 17:29:09 +0800 Subject: [PATCH 2/4] fix --- src/modelscope_hub/agent/__init__.py | 11 ++++------- src/modelscope_hub/agent/_api.py | 22 ---------------------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/src/modelscope_hub/agent/__init__.py b/src/modelscope_hub/agent/__init__.py index f07c50b..e9b19f8 100644 --- a/src/modelscope_hub/agent/__init__.py +++ b/src/modelscope_hub/agent/__init__.py @@ -11,12 +11,11 @@ (download/commit/LFS/list/create/delete). - :class:`RemoteFileInfo` -- metadata for a single remote file. - :func:`is_lfs_file` -- decide whether a file must use the LFS upload path. -- ``agent_visibility_label`` / ``agent_last_modified`` / ``agent_downloads`` / - ``agent_logo_url`` -- read renamed agent metadata fields from an API item, - tolerating both JSON spellings (snake_case and PascalCase) and legacy keys. +- ``agent_visibility_label`` / ``agent_last_modified`` -- read renamed agent + metadata fields from an API item, tolerating both JSON spellings + (snake_case and PascalCase) and legacy keys. """ -from ._api import (AgentApi, RemoteFileInfo, agent_downloads, - agent_last_modified, agent_logo_url, +from ._api import (AgentApi, RemoteFileInfo, agent_last_modified, agent_visibility_label, is_lfs_file) __all__ = [ @@ -25,6 +24,4 @@ "is_lfs_file", "agent_visibility_label", "agent_last_modified", - "agent_downloads", - "agent_logo_url", ] diff --git a/src/modelscope_hub/agent/_api.py b/src/modelscope_hub/agent/_api.py index 3c57625..afe9b0b 100644 --- a/src/modelscope_hub/agent/_api.py +++ b/src/modelscope_hub/agent/_api.py @@ -98,28 +98,6 @@ def agent_last_modified(item: dict) -> str: return "-" -def agent_downloads(item: dict) -> int: - """Read an agent's download count (``downloads``, was ``download_count``). - """ - for key in ("Downloads", "downloads", "DownloadCount", "download_count"): - val = item.get(key) - if val is not None: - try: - return int(val) - except (TypeError, ValueError): - return 0 - return 0 - - -def agent_logo_url(item: dict) -> str: - """Read an agent's logo URL (``logo_url``, formerly ``logo``).""" - for key in ("LogoUrl", "logo_url", "Logo", "logo"): - val = item.get(key) - if val: - return str(val) - return "" - - @dataclass class RemoteFileInfo: """Metadata for a single file in the remote repository.""" From e39bc8beecf8fa08f9d0f355a52470ebcd82deb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E6=B3=93?= Date: Wed, 5 Aug 2026 18:16:29 +0800 Subject: [PATCH 3/4] fix --- src/modelscope_hub/agent/__init__.py | 3 +-- src/modelscope_hub/cli/agent.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/modelscope_hub/agent/__init__.py b/src/modelscope_hub/agent/__init__.py index e9b19f8..bddddc6 100644 --- a/src/modelscope_hub/agent/__init__.py +++ b/src/modelscope_hub/agent/__init__.py @@ -15,8 +15,7 @@ metadata fields from an API item, tolerating both JSON spellings (snake_case and PascalCase) and legacy keys. """ -from ._api import (AgentApi, RemoteFileInfo, agent_last_modified, - agent_visibility_label, is_lfs_file) +from ._api import AgentApi, RemoteFileInfo, agent_last_modified, agent_visibility_label, is_lfs_file __all__ = [ "AgentApi", diff --git a/src/modelscope_hub/cli/agent.py b/src/modelscope_hub/cli/agent.py index f475f2f..3f8fe33 100644 --- a/src/modelscope_hub/cli/agent.py +++ b/src/modelscope_hub/cli/agent.py @@ -14,8 +14,7 @@ from argparse import RawDescriptionHelpFormatter from pathlib import Path -from ..agent import (AgentApi, agent_last_modified, agent_visibility_label, - is_lfs_file) +from ..agent import AgentApi, agent_last_modified, agent_visibility_label, is_lfs_file from ..constants import Visibility from ..errors import APIError from .base import CLICommand, SubParsers From da0a5a729c3514435956d6db57593a6c4f1db94c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E6=B3=93?= Date: Wed, 5 Aug 2026 20:03:19 +0800 Subject: [PATCH 4/4] fix --- src/modelscope_hub/agent/__init__.py | 1 + src/modelscope_hub/agent/_api.py | 9 +++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/modelscope_hub/agent/__init__.py b/src/modelscope_hub/agent/__init__.py index bddddc6..025d267 100644 --- a/src/modelscope_hub/agent/__init__.py +++ b/src/modelscope_hub/agent/__init__.py @@ -15,6 +15,7 @@ metadata fields from an API item, tolerating both JSON spellings (snake_case and PascalCase) and legacy keys. """ + from ._api import AgentApi, RemoteFileInfo, agent_last_modified, agent_visibility_label, is_lfs_file __all__ = [ diff --git a/src/modelscope_hub/agent/_api.py b/src/modelscope_hub/agent/_api.py index cb635b1..16effcc 100644 --- a/src/modelscope_hub/agent/_api.py +++ b/src/modelscope_hub/agent/_api.py @@ -104,8 +104,7 @@ def agent_visibility_label(item: dict) -> str: # because ``private=False`` means PUBLIC and would be skipped as falsy. for key in ("Private", "private"): if key in item and item[key] is not None: - return (Visibility.PRIVATE.label - if item[key] else Visibility.PUBLIC.label) + return Visibility.PRIVATE.label if item[key] else Visibility.PUBLIC.label # Legacy field, which arrived in several shapes: a label of any casing # (``"Public"``), an int enum (1/3/5) or its numeric string -- hence the @@ -134,8 +133,7 @@ def agent_last_modified(item: dict) -> str: UTC RFC3339, so it must not be shown as if it were local time. Older keys are accepted as a fallback. """ - for key in ("LastModified", "last_modified", "GmtModified", "gmt_modified", - "LastUpdatedDate", "last_updated_date"): + for key in ("LastModified", "last_modified", "GmtModified", "gmt_modified", "LastUpdatedDate", "last_updated_date"): val = item.get(key) if val: return str(val) @@ -294,8 +292,7 @@ def create_repo(self, path: str, name: str, framework: str | None = None, visibi """ allowed = (Visibility.PUBLIC.label, Visibility.PRIVATE.label) if visibility not in allowed: - raise ValueError( - f"visibility must be one of {allowed}, got {visibility!r}") + raise ValueError(f"visibility must be one of {allowed}, got {visibility!r}") # The agent API takes a boolean ``private`` (INVERTED semantics), not # the old ``visibility`` string. A string here would be rejected with # 400, and omitting it would silently default to public, so always