Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-modelscope-hub for 
openSUSE:Factory checked in at 2026-09-17 15:19:57
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-modelscope-hub (Old)
 and      /work/SRC/openSUSE:Factory/.python-modelscope-hub.new.383539 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-modelscope-hub"

Thu Sep 17 15:19:57 2026 rev:6 rq:1378300 version:0.4.3

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-modelscope-hub/python-modelscope-hub.changes  
    2026-09-10 17:40:54.495489746 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-modelscope-hub.new.383539/python-modelscope-hub.changes
  2026-09-17 15:21:16.704658221 +0200
@@ -1,0 +2,16 @@
+Wed Sep 16 07:24:49 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 0.4.3:
+  * Agent listing requires a read-scoped token and raises 403
+    OperationNotAllowed instead of returning an empty list
+  * MCP list pagination uses the requested page_number/page_size
+    when the service omits them, and rejects invalid page numbers
+  * Agent-IDP signing components are validated as ASCII without
+    '|'; Unicode encoding failures become E3021 InvalidParameter
+  * MCP list CLI drops the unsupported status column; HTTP-200
+    error envelopes decode to typed exceptions
+  * Re-export legacy modelscope SDK constants from
+    modelscope_hub.constants
+- Require pytest >= 7.0 to match upstream's test extra
+
+-------------------------------------------------------------------

Old:
----
  modelscope_hub-0.4.2.tar.gz

New:
----
  modelscope_hub-0.4.3.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-modelscope-hub.spec ++++++
--- /var/tmp/diff_new_pack.fO4BuL/_old  2026-09-17 15:21:17.327684353 +0200
+++ /var/tmp/diff_new_pack.fO4BuL/_new  2026-09-17 15:21:17.329684437 +0200
@@ -19,7 +19,7 @@
 %bcond_without libalternatives
 %{?sle15_python_module_pythons}
 Name:           python-modelscope-hub
-Version:        0.4.2
+Version:        0.4.3
 Release:        0
 Summary:        Official Python client for ModelScope Hub
 License:        Apache-2.0
@@ -29,7 +29,7 @@
 BuildRequires:  %{python_module cryptography >= 41}
 BuildRequires:  %{python_module filelock >= 3.9}
 BuildRequires:  %{python_module pip}
-BuildRequires:  %{python_module pytest}
+BuildRequires:  %{python_module pytest >= 7.0}
 BuildRequires:  %{python_module requests >= 2.28}
 BuildRequires:  %{python_module responses >= 0.20}
 BuildRequires:  %{python_module setuptools >= 68.0}

++++++ modelscope_hub-0.4.2.tar.gz -> modelscope_hub-0.4.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/modelscope_hub-0.4.2/PKG-INFO 
new/modelscope_hub-0.4.3/PKG-INFO
--- old/modelscope_hub-0.4.2/PKG-INFO   2026-09-10 04:59:09.073332300 +0200
+++ new/modelscope_hub-0.4.3/PKG-INFO   2026-09-15 14:09:21.563876600 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: modelscope-hub
-Version: 0.4.2
+Version: 0.4.3
 Summary: The official Python client to connect with ModelScope Hub.
 License: Apache-2.0
 Keywords: modelscope,hub,sdk,openapi,machine-learning
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/modelscope_hub-0.4.2/src/modelscope_hub/_openapi.py 
new/modelscope_hub-0.4.3/src/modelscope_hub/_openapi.py
--- old/modelscope_hub-0.4.2/src/modelscope_hub/_openapi.py     2026-09-10 
04:58:57.682107400 +0200
+++ new/modelscope_hub-0.4.3/src/modelscope_hub/_openapi.py     2026-09-15 
14:09:12.680884000 +0200
@@ -1085,6 +1085,62 @@
         return params
 
     @staticmethod
+    def _decode_mcp_list_response(response: requests.Response) -> JSON:
+        """Decode MCP list replies, including legacy HTTP-200 error envelopes.
+
+        The service normally signals errors through HTTP status codes. Some
+        deployments instead return ``200`` with ``success: false`` and an
+        OpenAPI-style error code. Treating that dictionary as a list payload
+        silently turned invalid request bodies into empty server lists.
+        """
+        try:
+            payload = response.json()
+        except ValueError:
+            return {"raw_response": response.text}
+        if not isinstance(payload, dict):
+            return payload
+
+        success = payload.get("success") if "success" in payload else 
payload.get("Success")
+        if success is False:
+            code = payload.get("code") if payload.get("code") is not None else 
payload.get("Code")
+            message = next(
+                (
+                    value.strip()
+                    for key in ("message", "Message", "msg", "Msg", "detail", 
"Detail")
+                    if isinstance(value := payload.get(key), str) and 
value.strip()
+                ),
+                "MCP server list request was rejected.",
+            )
+            request_id = payload.get("request_id") or payload.get("requestId") 
or payload.get("RequestId")
+            code_text = str(code).strip()
+            error_cls: type[APIError] = APIError
+            status_code = 400
+            if code_text == "InputParameterError":
+                error_cls = InvalidParameter
+            elif code_text == "InvalidAuthentication":
+                error_cls = AuthenticationError
+                status_code = 401
+            elif code_text == "OperationNotAllowed":
+                error_cls = PermissionDeniedError
+                status_code = 403
+            elif code_text == "RateLimitExceed":
+                error_cls = RateLimitError
+                status_code = 429
+            raise error_cls(
+                message,
+                status_code=status_code,
+                request_id=request_id,
+                response_body=payload,
+                url=response.url,
+                method=response.request.method if response.request else "PUT",
+            )
+        if "data" in payload:
+            return payload["data"]
+        if "Data" in payload:
+            return payload["Data"]
+        return payload
+
+    @staticmethod
     def _is_method_or_route_unsupported(exc: APIError) -> bool:
         return exc.status_code in (404, 405, 501)
 
@@ -1307,6 +1363,10 @@
         filter : dict, optional
             Nested filter object. Supported keys: ``category``, ``is_hosted``.
         """
+        if isinstance(page_number, bool) or not isinstance(page_number, int) 
or page_number < 1:
+            raise InvalidParameter("page_number must be an integer >= 1.")
+        if isinstance(page_size, bool) or not isinstance(page_size, int) or 
page_size < 1:
+            raise InvalidParameter("page_size must be an integer >= 1.")
         if page_number * page_size > 100:
             # The service enforces this itself, answering 403 QuotaLimitExceed 
with
             # exactly this rule. Checking here spares the round trip and 
reports it
@@ -1326,14 +1386,16 @@
         body = {k: v for k, v in body.items() if v is not None}
 
         def _get() -> JSON:
-            return self._request(
+            response = self._request(
                 "GET",
                 "/mcp/servers",
                 params=self._flatten_mcp_list_params(body),
                 require_token=False,
                 required_scope=TokenScope.READ,
                 anonymous_retry=True,
+                unwrap=False,
             )
+            return self._decode_mcp_list_response(response)
 
         if self._mcp_list_supports_get:
             # This deployment already answered GET and refused PUT, so leading
@@ -1341,14 +1403,16 @@
             return _get()
 
         try:
-            return self._request(
+            response = self._request(
                 "PUT",
                 "/mcp/servers",
                 json_body=body,
                 require_token=False,
                 required_scope=TokenScope.READ,
                 anonymous_retry=True,
+                unwrap=False,
             )
+            return self._decode_mcp_list_response(response)
         except APIError as exc:
             if self._mcp_list_supports_get is False or not 
self._is_method_or_route_unsupported(exc):
                 raise
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/modelscope_hub-0.4.2/src/modelscope_hub/agent/_api.py 
new/modelscope_hub-0.4.3/src/modelscope_hub/agent/_api.py
--- old/modelscope_hub-0.4.2/src/modelscope_hub/agent/_api.py   2026-09-10 
04:58:57.682107400 +0200
+++ new/modelscope_hub-0.4.3/src/modelscope_hub/agent/_api.py   2026-09-15 
14:09:12.681884000 +0200
@@ -22,8 +22,8 @@
 
 from .._openapi import OpenAPIClient
 from ..config import HubConfig
-from ..constants import Visibility
-from ..errors import AuthenticationError, NotExistError
+from ..constants import TokenScope, Visibility
+from ..errors import APIError, AuthenticationError, NotExistError, 
PermissionDeniedError
 
 logger = logging.getLogger("modelscope_hub.agent")
 
@@ -230,12 +230,88 @@
         """True if the repo exists, False on 404."""
         return self.repo_info(path, name) is not None
 
+    @staticmethod
+    def _decode_dolphin_list_response(response: object, *, list_url: str) -> 
object:
+        """Decode the legacy dolphin list envelope without hiding soft errors.
+
+        Most endpoints signal access denial with HTTP 403, which OpenAPIClient
+        maps normally. ``/api/v1/dolphin/agents`` can instead answer HTTP 200
+        with ``Code=OperationNotAllowed``. Its former decode path then treated
+        the payload as an empty result, making a scope error indistinguishable
+        from an owner with no repositories.
+        """
+        try:
+            payload = response.json()  # type: ignore[union-attr]
+        except (AttributeError, ValueError) as exc:
+            raise APIError(
+                "Agent repository list endpoint returned a non-JSON response.",
+                status_code=500,
+                url=list_url,
+                method="PUT",
+            ) from exc
+
+        if not isinstance(payload, dict):
+            return payload
+
+        code = payload.get("Code") if payload.get("Code") is not None else 
payload.get("code")
+        success = payload.get("Success") if "Success" in payload else 
payload.get("success")
+        message = next(
+            (
+                value.strip()
+                for key in ("Message", "message", "Msg", "msg")
+                if isinstance(value := payload.get(key), str) and value.strip()
+            ),
+            "Agent repository list endpoint rejected the request.",
+        )
+        code_text = str(code).strip()
+        message_lower = message.lower()
+        permission_denied = (
+            code_text == "OperationNotAllowed"
+            or code_text == "403"
+            or "operationnotallowed" in message_lower
+            or "permission denied" in message_lower
+            or "not allowed" in message_lower
+            or "forbidden" in message_lower
+        )
+        failed_envelope = success is False or (code is not None and code_text 
not in ("", "0", "200"))
+        if permission_denied:
+            error = PermissionDeniedError(
+                f"OperationNotAllowed: {message}",
+                status_code=403,
+                request_id=payload.get("RequestId") or 
payload.get("request_id"),
+                response_body=payload,
+                url=list_url,
+                method="PUT",
+            )
+            error.suggestion = (
+                "Agent repository listing requires a token with 'read' 
permission; "
+                "an api-inference-only token cannot access this endpoint."
+            )
+            raise error
+        if failed_envelope:
+            raise APIError(
+                f"Agent repository list endpoint rejected the request: 
{message}",
+                status_code=400,
+                request_id=payload.get("RequestId") or 
payload.get("request_id"),
+                response_body=payload,
+                url=list_url,
+                method="PUT",
+            )
+
+        # Match OpenAPIClient's success-envelope unwrapping while preserving 
the
+        # outer envelope long enough to inspect soft errors above.
+        if "data" in payload:
+            return payload["data"]
+        if "Data" in payload:
+            return payload["Data"]
+        return payload
+
     def list_agents(self, owner: str | None = None, page_number: int = 1, 
page_size: int = 10) -> dict:
         """List agent repositories (PUT /api/v1/dolphin/agents).
 
-        Queries the dolphin search endpoint. When *owner* is given it is sent 
as
-        a ``Path contains`` criterion (the group filter). Returns a dict with
-        'items' (list of agent metadata dicts) and 'total_count' (int).
+        This is an account-scoped operation, not a public catalogue lookup. It
+        requires a token with ``read`` permission and must never downgrade a
+        denied request into an anonymous empty list.
         """
         criterion: list[dict] = []
         if owner:
@@ -254,7 +330,15 @@
             "Criterion": criterion,
         }
         list_url = f"{self.server}/api/v1/dolphin/agents"
-        data = self._openapi.request("PUT", url=list_url, json_body=body, 
require_token=False)
+        response = self._openapi._request(
+            "PUT",
+            url=list_url,
+            json_body=body,
+            require_token=True,
+            required_scope=TokenScope.READ,
+            unwrap=False,
+        )
+        data = self._decode_dolphin_list_response(response, list_url=list_url)
         if isinstance(data, list):
             return {"items": data, "total_count": len(data)}
         if isinstance(data, dict):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/modelscope_hub-0.4.2/src/modelscope_hub/agent_idp.py 
new/modelscope_hub-0.4.3/src/modelscope_hub/agent_idp.py
--- old/modelscope_hub-0.4.2/src/modelscope_hub/agent_idp.py    2026-09-10 
04:58:57.682107400 +0200
+++ new/modelscope_hub-0.4.3/src/modelscope_hub/agent_idp.py    2026-09-15 
14:09:12.681884000 +0200
@@ -54,6 +54,30 @@
     return decoded
 
 
+def _canonical_signing_component(value: str, field: str) -> str:
+    """Validate one component of the server-defined ASCII signing message.
+
+    The Agent-IDP protocol signs the literal
+    ``agent_id|kid|audience|timestamp`` byte sequence.  ``audience`` is the
+    target Hub application's ``client_id``, not a display name.  Encoding a
+    Unicode display name as UTF-8 here would silently change the bytes the
+    service verifies, so reject it explicitly instead of leaking a Python
+    ``UnicodeEncodeError``.
+    """
+    try:
+        value.encode("ascii")
+    except UnicodeEncodeError:
+        reason = (
+            "the target Hub application's client_id"
+            if field == "audience"
+            else "the canonical Agent-IDP signature message"
+        )
+        raise InvalidParameter(f"{field} must contain only ASCII characters 
because it is {reason}.") from None
+    if "|" in value:
+        raise InvalidParameter(f"{field} must not contain '|', the Agent-IDP 
signature message delimiter.")
+    return value
+
+
 def _normalise_private_jwk(value: AgentJWK | Mapping[str, Any]) -> AgentJWK:
     """Validate an Ed25519 private JWK and return a safe structured copy."""
     if isinstance(value, AgentJWK):
@@ -186,20 +210,28 @@
     audience: str,
     timestamp: int,
 ) -> TokenSignPayload:
-    """Build the signed body required by anonymous ``POST /agent_id/token``."""
+    """Build the signed body required by anonymous ``POST /agent_id/token``.
+
+    ``audience`` must be the ASCII Hub application ``client_id``. It is not a
+    human-readable application name: every component is signed as the literal
+    ASCII ``agent_id|kid|audience|timestamp`` protocol byte sequence.
+    """
     if not isinstance(agent_id, str) or not agent_id:
         raise InvalidParameter("agent_id must be a non-empty string.")
     if not isinstance(audience, str) or not audience:
         raise InvalidParameter("audience must be a non-empty string.")
     if isinstance(timestamp, bool) or not isinstance(timestamp, int) or 
timestamp <= 0:
         raise InvalidParameter("timestamp must be a positive Unix timestamp in 
seconds.")
+    agent_id = _canonical_signing_component(agent_id, "agent_id")
+    audience = _canonical_signing_component(audience, "audience")
     key = _normalise_private_jwk(private_jwk)
-    message = f"{agent_id}|{key.kid}|{audience}|{timestamp}".encode("ascii")
+    kid = _canonical_signing_component(key.kid, "kid")
+    message = f"{agent_id}|{kid}|{audience}|{timestamp}".encode("ascii")
     private_bytes = _decode_base64url(key.d, "d")
     signature = 
Ed25519PrivateKey.from_private_bytes(private_bytes).sign(message)
     return {
         "agent_id": agent_id,
-        "kid": key.kid,
+        "kid": kid,
         "audience": audience,
         "timestamp": timestamp,
         "signature": _encode_base64url(signature),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/modelscope_hub-0.4.2/src/modelscope_hub/api.py 
new/modelscope_hub-0.4.3/src/modelscope_hub/api.py
--- old/modelscope_hub-0.4.2/src/modelscope_hub/api.py  2026-09-10 
04:58:57.683107600 +0200
+++ new/modelscope_hub-0.4.3/src/modelscope_hub/api.py  2026-09-15 
14:09:12.681884000 +0200
@@ -1707,11 +1707,9 @@
         """
         rt = self._normalize_repo_type(repo_type)
         paths = self._normalize_delete_values(file_paths, "file_paths")
-        patterns = self._normalize_delete_values(
-            delete_patterns, "delete_patterns")
+        patterns = self._normalize_delete_values(delete_patterns, 
"delete_patterns")
         if not paths and not patterns:
-            raise InvalidParameter(
-                "Provide at least one file path or delete pattern.")
+            raise InvalidParameter("Provide at least one file path or delete 
pattern.")
 
         resolved_revision = revision or "master"
         if patterns:
@@ -1726,9 +1724,8 @@
                 if file.path and file.type != "tree"
             ]
             paths.extend(
-                path for path in remote_paths
-                if any(fnmatch.fnmatchcase(path, pattern)
-                       for pattern in patterns))
+                path for path in remote_paths if any(fnmatch.fnmatchcase(path, 
pattern) for pattern in patterns)
+            )
 
         paths = list(dict.fromkeys(paths))
         if not paths:
@@ -1758,8 +1755,7 @@
         normalized: list[str] = []
         for value in raw_values:
             if not isinstance(value, str):
-                raise InvalidParameter(
-                    f"{parameter_name} must contain only strings.")
+                raise InvalidParameter(f"{parameter_name} must contain only 
strings.")
             if value:
                 normalized.append(value)
         return normalized
@@ -2240,8 +2236,16 @@
             filter=filter,
             extra={k: v for k, v in extra.items() if v is not None} or None,
         )
-        items, total, page, size = self._extract_paged(payload)
-        return PagedResult(items=list(items), total_count=total, 
page_number=page, page_size=size)
+        items, total, _page, _size = self._extract_paged(payload)
+        # The MCP service currently omits page_number/page_size in its 
response.
+        # The request values are authoritative, exactly as in 
list_repos("mcp"),
+        # so callers can reliably tell which page they received.
+        return PagedResult(
+            items=list(items),
+            total_count=total,
+            page_number=page_number,
+            page_size=page_size,
+        )
 
     def list_operational_mcp_servers(self) -> PagedResult[dict]:
         """List the MCP servers the caller currently has hosted.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/modelscope_hub-0.4.2/src/modelscope_hub/cli/agent.py 
new/modelscope_hub-0.4.3/src/modelscope_hub/cli/agent.py
--- old/modelscope_hub-0.4.2/src/modelscope_hub/cli/agent.py    2026-09-10 
04:58:57.683107600 +0200
+++ new/modelscope_hub-0.4.3/src/modelscope_hub/cli/agent.py    2026-09-15 
14:09:12.682884000 +0200
@@ -35,11 +35,27 @@
     return 1
 
 
+def _is_operation_not_allowed(e: APIError) -> bool:
+    """Recognise the server's permission code in either message or envelope."""
+    if "OperationNotAllowed" in e.message:
+        return True
+    body = e.response_body
+    if not isinstance(body, dict):
+        return False
+    code = body.get("Code") if body.get("Code") is not None else 
body.get("code")
+    return str(code) == "OperationNotAllowed"
+
+
 def _api_error_message(e: APIError, action: str = "request") -> str:
     status = e.status_code or 0
     if status == 401:
         return "authentication failed. Please login again."
     if status == 403:
+        if action == "list" and _is_operation_not_allowed(e):
+            return (
+                "permission denied (403 OperationNotAllowed). Agent repository 
listing requires a token with "
+                "'read' permission; an api-inference-only token cannot access 
this endpoint."
+            )
         return "permission denied. You do not have access to this resource."
     if status == 404:
         return "resource not found. Check the repository name and try again."
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/modelscope_hub-0.4.2/src/modelscope_hub/cli/mcp.py 
new/modelscope_hub-0.4.3/src/modelscope_hub/cli/mcp.py
--- old/modelscope_hub-0.4.2/src/modelscope_hub/cli/mcp.py      2026-09-10 
04:58:57.684107500 +0200
+++ new/modelscope_hub-0.4.3/src/modelscope_hub/cli/mcp.py      2026-09-15 
14:09:12.682884000 +0200
@@ -64,12 +64,11 @@
             (
                 item.get("id") or item.get("Id") or "-",
                 item.get("name") or item.get("Name") or "-",
-                item.get("status") or item.get("Status") or "-",
                 item.get("description") or item.get("Description") or "-",
             )
             for item in result.items
         ]
-        info(render_table(rows, headers=["id", "name", "status", 
"description"]))
+        info(render_table(rows, headers=["id", "name", "description"]))
         info(f"\npage {result.page_number} / total {result.total_count}")
 
     @staticmethod
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/modelscope_hub-0.4.2/src/modelscope_hub/compat/constants.py 
new/modelscope_hub-0.4.3/src/modelscope_hub/compat/constants.py
--- old/modelscope_hub-0.4.2/src/modelscope_hub/compat/constants.py     
2026-09-10 04:58:57.684107500 +0200
+++ new/modelscope_hub-0.4.3/src/modelscope_hub/compat/constants.py     
2026-09-15 14:09:12.683884000 +0200
@@ -1,75 +1,78 @@
-"""Legacy constant mappings for backward compatibility with modelscope SDK."""
+"""Legacy constant mappings for backward compatibility with the modelscope SDK.
 
-from ..constants import (
+Every name here is re-exported from :mod:`modelscope_hub.constants`, the single
+source of truth. This module defines no independent values of its own; it only
+surfaces the historical names (plus a few integer / env-name aliases) that the
+modelscope SDK imports.
+"""
+
+from ..constants import (  # noqa: F401
+    DEFAULT_CREDENTIALS_PATH,
+    DEFAULT_DATASET_REVISION,
+    DEFAULT_MAX_WORKERS,
+    DEFAULT_MODELSCOPE_DATA_ENDPOINT,
+    DEFAULT_MODELSCOPE_DOMAIN,
+    DEFAULT_MODELSCOPE_GROUP,
+    DEFAULT_MODELSCOPE_INTL_DATA_ENDPOINT,
+    DEFAULT_MODELSCOPE_INTL_DOMAIN,
+    DEFAULT_SKILLS_DIR,
+    FILE_HASH,
+    MODEL_ID_SEPARATOR,
+    REPO_TYPE_DATASET,
+    REPO_TYPE_MODEL,
+    REPO_TYPE_STUDIO,
+    REPO_TYPE_SUPPORT,
+    TEMPORARY_FOLDER_NAME,
+    UPLOAD_ADAPTIVE_BATCH_SIZE,
     UPLOAD_ADAPTIVE_BATCHING_ENABLED,
+    UPLOAD_BLOB_CONNECT_TIMEOUT,
     UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS,
     UPLOAD_BLOB_MAX_ATTEMPTS,
+    UPLOAD_BLOB_MAX_RETRIES,
     UPLOAD_BLOB_PROGRESS_THRESHOLD_BYTES,
+    UPLOAD_BLOB_READ_TIMEOUT,
     UPLOAD_BLOB_READ_TIMEOUT_SECONDS,
+    UPLOAD_BLOB_RETRY_BACKOFF,
     UPLOAD_BLOB_RETRY_BACKOFF_BASE_SECONDS,
     UPLOAD_BLOB_RETRY_MAX_DELAY_SECONDS,
+    UPLOAD_BLOB_RETRY_MAX_WAIT,
+    UPLOAD_BLOB_TIMEOUT,
+    UPLOAD_BLOB_TQDM_DISABLE_THRESHOLD,
     UPLOAD_BLOB_VALIDATION_BATCH_MAX_OBJECTS,
     UPLOAD_CACHE_ENABLED,
     UPLOAD_COMMIT_BATCH_MAX_OPERATIONS,
+    UPLOAD_COMMIT_BATCH_SIZE,
+    UPLOAD_FAILED_FILE_MAX_RETRIES,
     UPLOAD_FAILED_FILE_MAX_RETRY_ROUNDS,
     UPLOAD_HTTP_RETRY_ALLOWED_METHODS,
     UPLOAD_LFS_FORCE_THRESHOLD_BYTES,
+    UPLOAD_LFS_THRESHOLD,
     UPLOAD_MAX_CONCURRENT_WORKERS,
+    UPLOAD_MAX_FILE_COUNT,
+    UPLOAD_MAX_FILE_COUNT_IN_DIR,
+    UPLOAD_MAX_FILE_SIZE,
     UPLOAD_MAX_FILE_SIZE_BYTES,
     UPLOAD_MAX_FILES_PER_DIRECTORY,
+    UPLOAD_NORMAL_FILE_SIZE_TOTAL_LIMIT,
     UPLOAD_NORMAL_FILES_TOTAL_SIZE_BYTES,
+    UPLOAD_REACT_BACKOFF_MAX_EXPONENT,
+    UPLOAD_REACT_ENABLED,
+    UPLOAD_REACT_MAX_DELAY,
+    UPLOAD_REACT_ROUND2_BASE_DELAY,
+    UPLOAD_REACT_ROUND3_FILE_DELAY,
     UPLOAD_RECOVERY_BACKOFF_MAX_EXPONENT,
     UPLOAD_RECOVERY_ENABLED,
     UPLOAD_RECOVERY_MAX_DELAY_SECONDS,
     UPLOAD_RECOVERY_SERIAL_BACKOFF_BASE_SECONDS,
     UPLOAD_RECOVERY_SINGLE_FILE_DELAY_SECONDS,
+    UPLOAD_RETRY_ALLOWED_METHODS,
+    UPLOAD_SIZE_THRESHOLD_TO_ENFORCE_LFS,
+    UPLOAD_USE_CACHE,
+    UPLOAD_VALIDATE_BLOB_BATCH_SIZE,
     RepoType,
     Visibility,
-    get_upload_ignore_file_pattern,  # noqa: F401
+    get_upload_ignore_file_pattern,
 )
-from ..constants import (
-    UPLOAD_LFS_THRESHOLD as _UPLOAD_LFS_THRESHOLD,
-)
-from ..constants import (
-    UPLOAD_MAX_FILE_COUNT as _UPLOAD_MAX_FILE_COUNT,
-)
-
-REPO_TYPE_MODEL: str = RepoType.MODEL.value
-REPO_TYPE_DATASET: str = RepoType.DATASET.value
-REPO_TYPE_STUDIO: str = RepoType.STUDIO.value
-REPO_TYPE_SUPPORT: list[str] = [REPO_TYPE_MODEL, REPO_TYPE_DATASET, 
REPO_TYPE_STUDIO]
-
-DEFAULT_DATASET_REVISION: str = "master"
-DEFAULT_MAX_WORKERS: int = UPLOAD_MAX_CONCURRENT_WORKERS
-
-# Legacy upload constants. Values come from the modelscope-hub runtime config.
-UPLOAD_BLOB_CONNECT_TIMEOUT: int = UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS
-UPLOAD_BLOB_READ_TIMEOUT: int = UPLOAD_BLOB_READ_TIMEOUT_SECONDS
-UPLOAD_BLOB_MAX_RETRIES: int = UPLOAD_BLOB_MAX_ATTEMPTS
-UPLOAD_BLOB_RETRY_BACKOFF: int = UPLOAD_BLOB_RETRY_BACKOFF_BASE_SECONDS
-UPLOAD_BLOB_RETRY_MAX_WAIT: int = UPLOAD_BLOB_RETRY_MAX_DELAY_SECONDS
-UPLOAD_FAILED_FILE_MAX_RETRIES: int = UPLOAD_FAILED_FILE_MAX_RETRY_ROUNDS
-UPLOAD_BLOB_TIMEOUT: tuple[int, int] = (
-    UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS,
-    UPLOAD_BLOB_READ_TIMEOUT_SECONDS,
-)
-UPLOAD_RETRY_ALLOWED_METHODS: frozenset[str] = 
UPLOAD_HTTP_RETRY_ALLOWED_METHODS
-UPLOAD_MAX_FILE_SIZE: int = UPLOAD_MAX_FILE_SIZE_BYTES
-UPLOAD_MAX_FILE_COUNT: int = _UPLOAD_MAX_FILE_COUNT
-UPLOAD_SIZE_THRESHOLD_TO_ENFORCE_LFS: int = UPLOAD_LFS_FORCE_THRESHOLD_BYTES
-UPLOAD_LFS_THRESHOLD: int = _UPLOAD_LFS_THRESHOLD
-UPLOAD_MAX_FILE_COUNT_IN_DIR: int = UPLOAD_MAX_FILES_PER_DIRECTORY
-UPLOAD_NORMAL_FILE_SIZE_TOTAL_LIMIT: int = UPLOAD_NORMAL_FILES_TOTAL_SIZE_BYTES
-UPLOAD_COMMIT_BATCH_SIZE: int = UPLOAD_COMMIT_BATCH_MAX_OPERATIONS
-UPLOAD_VALIDATE_BLOB_BATCH_SIZE: int = UPLOAD_BLOB_VALIDATION_BATCH_MAX_OBJECTS
-UPLOAD_ADAPTIVE_BATCH_SIZE: bool = UPLOAD_ADAPTIVE_BATCHING_ENABLED
-UPLOAD_REACT_ENABLED: bool = UPLOAD_RECOVERY_ENABLED
-UPLOAD_REACT_ROUND2_BASE_DELAY: int = 
UPLOAD_RECOVERY_SERIAL_BACKOFF_BASE_SECONDS
-UPLOAD_REACT_ROUND3_FILE_DELAY: int = UPLOAD_RECOVERY_SINGLE_FILE_DELAY_SECONDS
-UPLOAD_REACT_BACKOFF_MAX_EXPONENT: int = UPLOAD_RECOVERY_BACKOFF_MAX_EXPONENT
-UPLOAD_REACT_MAX_DELAY: int = UPLOAD_RECOVERY_MAX_DELAY_SECONDS
-UPLOAD_BLOB_TQDM_DISABLE_THRESHOLD: int = UPLOAD_BLOB_PROGRESS_THRESHOLD_BYTES
-UPLOAD_USE_CACHE: bool = UPLOAD_CACHE_ENABLED
 
 # Visibility integer constants matching the old SDK
 ModelVisibility_PUBLIC: int = int(Visibility.PUBLIC)
@@ -79,9 +82,3 @@
 # Env var name constants (old SDK imports these as string constants)
 MODELSCOPE_DOMAIN: str = "MODELSCOPE_DOMAIN"
 MODELSCOPE_PREFER_AI_SITE: str = "MODELSCOPE_PREFER_AI_SITE"
-
-# API response field names
-FILE_HASH: str = "Sha256"
-
-# Download temp folder name
-TEMPORARY_FOLDER_NAME: str = "._____temp"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/modelscope_hub-0.4.2/src/modelscope_hub/constants.py 
new/modelscope_hub-0.4.3/src/modelscope_hub/constants.py
--- old/modelscope_hub-0.4.2/src/modelscope_hub/constants.py    2026-09-10 
04:58:57.685107700 +0200
+++ new/modelscope_hub-0.4.3/src/modelscope_hub/constants.py    2026-09-15 
14:09:12.683884000 +0200
@@ -12,6 +12,7 @@
 import warnings
 from dataclasses import dataclass
 from enum import Enum, IntEnum
+from pathlib import Path
 
 # ---------------------------------------------------------------------------
 # StrEnum compatibility shim (Python 3.10 lacks :class:`enum.StrEnum`).
@@ -183,6 +184,38 @@
 
 
 # ---------------------------------------------------------------------------
+# Repo-type string aliases and shared repo defaults.
+#
+# The modern surface models repo kinds as :class:`RepoType`; these string
+# aliases and the dataset-revision default are the historical names the
+# modelscope SDK consumes. Defined here so ``modelscope_hub.constants`` is the
+# single source of truth (``compat.constants`` re-exports them).
+# ---------------------------------------------------------------------------
+REPO_TYPE_MODEL: str = RepoType.MODEL.value
+REPO_TYPE_DATASET: str = RepoType.DATASET.value
+REPO_TYPE_STUDIO: str = RepoType.STUDIO.value
+REPO_TYPE_SUPPORT: list[str] = [REPO_TYPE_MODEL, REPO_TYPE_DATASET, 
REPO_TYPE_STUDIO]
+DEFAULT_DATASET_REVISION: str = "master"
+
+# ---------------------------------------------------------------------------
+# Legacy modelscope domain / endpoint / filesystem constants.
+#
+# Historical names and shapes (``www.`` domains, the ``damo`` group, a ``Path``
+# credentials location) the modelscope SDK consumes. The modern canonical
+# endpoint stays :data:`DEFAULT_ENDPOINT` (no ``www``); these coexist for
+# backward compatibility and ``compat.constants`` re-exports them.
+# ---------------------------------------------------------------------------
+MODEL_ID_SEPARATOR: str = "/"
+DEFAULT_MODELSCOPE_GROUP: str = "damo"
+DEFAULT_MODELSCOPE_DOMAIN: str = "www.modelscope.cn"
+DEFAULT_MODELSCOPE_INTL_DOMAIN: str = "www.modelscope.ai"
+DEFAULT_MODELSCOPE_DATA_ENDPOINT: str = "https://"; + DEFAULT_MODELSCOPE_DOMAIN
+DEFAULT_MODELSCOPE_INTL_DATA_ENDPOINT: str = "https://"; + 
DEFAULT_MODELSCOPE_INTL_DOMAIN
+DEFAULT_SKILLS_DIR: str = os.path.join(os.path.expanduser("~"), ".agents", 
"skills")
+DEFAULT_CREDENTIALS_PATH: Path = Path.home().joinpath(".modelscope", 
"credentials")
+
+
+# ---------------------------------------------------------------------------
 # Helpers for environment-driven overrides (auto-registering)
 # ---------------------------------------------------------------------------
 _REGISTERED_NAMES: set[str] = set()
@@ -503,6 +536,9 @@
 FILE_HASH_FIELD: str = "Sha256"
 """API response field name for file hash."""
 
+FILE_HASH: str = FILE_HASH_FIELD
+"""Legacy alias of :data:`FILE_HASH_FIELD` (modelscope SDK name)."""
+
 ENV_FILE_LOCK: str = "MODELSCOPE_DOWNLOAD_FILE_LOCK"
 _env_register(
     ENV_FILE_LOCK,
@@ -755,6 +791,7 @@
 # Deprecated Python aliases. Runtime code must use the explicit names above.
 UPLOAD_BLOB_CONNECT_TIMEOUT = UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS
 UPLOAD_BLOB_READ_TIMEOUT = UPLOAD_BLOB_READ_TIMEOUT_SECONDS
+UPLOAD_BLOB_TIMEOUT = (UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS, 
UPLOAD_BLOB_READ_TIMEOUT_SECONDS)
 UPLOAD_BLOB_MAX_RETRIES = UPLOAD_BLOB_MAX_ATTEMPTS
 UPLOAD_BLOB_RETRY_BACKOFF = UPLOAD_BLOB_RETRY_BACKOFF_BASE_SECONDS
 UPLOAD_BLOB_RETRY_MAX_WAIT = UPLOAD_BLOB_RETRY_MAX_DELAY_SECONDS
@@ -775,6 +812,7 @@
 DEFAULT_MAX_WORKERS = UPLOAD_MAX_CONCURRENT_WORKERS
 UPLOAD_USE_CACHE = UPLOAD_CACHE_ENABLED
 UPLOAD_LFS_ENFORCE_THRESHOLD = UPLOAD_LFS_FORCE_THRESHOLD_BYTES
+UPLOAD_SIZE_THRESHOLD_TO_ENFORCE_LFS = UPLOAD_LFS_FORCE_THRESHOLD_BYTES
 UPLOAD_MAX_FILE_COUNT_IN_DIR = UPLOAD_MAX_FILES_PER_DIRECTORY
 UPLOAD_MAX_FILE_SIZE = UPLOAD_MAX_FILE_SIZE_BYTES
 UPLOAD_NORMAL_FILE_SIZE_TOTAL_LIMIT = UPLOAD_NORMAL_FILES_TOTAL_SIZE_BYTES
@@ -923,10 +961,18 @@
     "CONFIG_DIR_NAME",
     "DATASET_LFS_SUFFIX",
     "DEFAULT_CACHE_DIR_NAME",
+    "DEFAULT_CREDENTIALS_PATH",
+    "DEFAULT_DATASET_REVISION",
     "DEFAULT_ENDPOINT",
     "DEFAULT_IGNORE_PATTERNS",
     "DEFAULT_INTL_ENDPOINT",
     "DEFAULT_MAX_WORKERS",
+    "DEFAULT_MODELSCOPE_DATA_ENDPOINT",
+    "DEFAULT_MODELSCOPE_DOMAIN",
+    "DEFAULT_MODELSCOPE_GROUP",
+    "DEFAULT_MODELSCOPE_INTL_DATA_ENDPOINT",
+    "DEFAULT_MODELSCOPE_INTL_DOMAIN",
+    "DEFAULT_SKILLS_DIR",
     "DOWNLOAD_CHUNK_SIZE",
     "DOWNLOAD_PARALLEL_THRESHOLD",
     "DOWNLOAD_PARALLELS",
@@ -942,12 +988,18 @@
     "ENV_PREFER_AI_SITE",
     "ENV_REGISTRY",
     "EnvVar",
+    "FILE_HASH",
     "FILE_HASH_FIELD",
     "get_upload_ignore_file_pattern",
     "LEGACY_API_PREFIX",
     "License",
+    "MODEL_ID_SEPARATOR",
     "MODEL_LFS_SUFFIX",
     "OPENAPI_PREFIX",
+    "REPO_TYPE_DATASET",
+    "REPO_TYPE_MODEL",
+    "REPO_TYPE_STUDIO",
+    "REPO_TYPE_SUPPORT",
     "RepoType",
     "StrEnum",
     "SESSION_FILE_NAME",
@@ -968,6 +1020,7 @@
     "UPLOAD_BLOB_RETRY_BACKOFF_BASE_SECONDS",
     "UPLOAD_BLOB_RETRY_MAX_DELAY_SECONDS",
     "UPLOAD_BLOB_RETRY_MAX_WAIT",
+    "UPLOAD_BLOB_TIMEOUT",
     "UPLOAD_BLOB_TQDM_DISABLE_THRESHOLD",
     "UPLOAD_BLOB_VALIDATION_BATCH_MAX_OBJECTS",
     "UPLOAD_CACHE_ENABLED",
@@ -1005,6 +1058,7 @@
     "UPLOAD_RECOVERY_SERIAL_BACKOFF_BASE_SECONDS",
     "UPLOAD_RECOVERY_SINGLE_FILE_DELAY_SECONDS",
     "UPLOAD_RETRY_ALLOWED_METHODS",
+    "UPLOAD_SIZE_THRESHOLD_TO_ENFORCE_LFS",
     "UPLOAD_USE_CACHE",
     "UPLOAD_VALIDATE_BLOB_BATCH_SIZE",
     "Visibility",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/modelscope_hub-0.4.2/src/modelscope_hub/version.py 
new/modelscope_hub-0.4.3/src/modelscope_hub/version.py
--- old/modelscope_hub-0.4.2/src/modelscope_hub/version.py      2026-09-10 
04:58:57.685107700 +0200
+++ new/modelscope_hub-0.4.3/src/modelscope_hub/version.py      2026-09-15 
14:09:12.684884000 +0200
@@ -1,3 +1,3 @@
 """Version information for modelscope_hub."""
 
-__version__ = "0.4.2"
+__version__ = "0.4.3"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/modelscope_hub-0.4.2/src/modelscope_hub.egg-info/PKG-INFO 
new/modelscope_hub-0.4.3/src/modelscope_hub.egg-info/PKG-INFO
--- old/modelscope_hub-0.4.2/src/modelscope_hub.egg-info/PKG-INFO       
2026-09-10 04:59:09.045331700 +0200
+++ new/modelscope_hub-0.4.3/src/modelscope_hub.egg-info/PKG-INFO       
2026-09-15 14:09:21.534876600 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: modelscope-hub
-Version: 0.4.2
+Version: 0.4.3
 Summary: The official Python client to connect with ModelScope Hub.
 License: Apache-2.0
 Keywords: modelscope,hub,sdk,openapi,machine-learning
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/modelscope_hub-0.4.2/src/modelscope_hub.egg-info/SOURCES.txt 
new/modelscope_hub-0.4.3/src/modelscope_hub.egg-info/SOURCES.txt
--- old/modelscope_hub-0.4.2/src/modelscope_hub.egg-info/SOURCES.txt    
2026-09-10 04:59:09.054332000 +0200
+++ new/modelscope_hub-0.4.3/src/modelscope_hub.egg-info/SOURCES.txt    
2026-09-15 14:09:21.544876600 +0200
@@ -55,6 +55,7 @@
 tests/test_agent_idp.py
 tests/test_agent_idp_keys.py
 tests/test_cache_verification.py
+tests/test_compat_constants_completeness.py
 tests/test_compat_delete_files.py
 tests/test_compat_get_model_files.py
 tests/test_compat_snapshot_download.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/modelscope_hub-0.4.2/tests/test_agent_idp.py 
new/modelscope_hub-0.4.3/tests/test_agent_idp.py
--- old/modelscope_hub-0.4.2/tests/test_agent_idp.py    2026-09-10 
04:58:57.689107700 +0200
+++ new/modelscope_hub-0.4.3/tests/test_agent_idp.py    2026-09-15 
14:09:12.688884000 +0200
@@ -8,6 +8,7 @@
 import requests
 
 from modelscope_hub._openapi import OpenAPIClient
+from modelscope_hub.agent_idp import generate_agent_key_pair
 from modelscope_hub.api import HubApi
 from modelscope_hub.config import HubConfig
 from modelscope_hub.errors import InvalidParameter
@@ -145,3 +146,17 @@
         )
         assert isinstance(token, AgentToken)
         assert token.access_token == "jwt"
+
+    def test_private_key_facade_rejects_non_ascii_audience_before_http(self):
+        private_jwk, _ = generate_agent_key_pair("key-1")
+        api = HubApi(token="test-token")
+        api._openapi = MagicMock()
+        with pytest.raises(InvalidParameter, match="audience must contain only 
ASCII characters") as raised:
+            api.issue_agent_token_with_private_key(
+                private_jwk,
+                agent_id="agent_id:modelscope:agent_xxx",
+                audience="高德地图",
+                timestamp=1700000000,
+            )
+        assert raised.value.error_code == "E3021"
+        api._openapi.issue_agent_token.assert_not_called()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/modelscope_hub-0.4.2/tests/test_agent_idp_keys.py 
new/modelscope_hub-0.4.3/tests/test_agent_idp_keys.py
--- old/modelscope_hub-0.4.2/tests/test_agent_idp_keys.py       2026-09-10 
04:58:57.689107700 +0200
+++ new/modelscope_hub-0.4.3/tests/test_agent_idp_keys.py       2026-09-15 
14:09:12.688884000 +0200
@@ -73,3 +73,36 @@
     private_jwk, _ = generate_agent_key_pair()
     with pytest.raises(InvalidParameter, match="timestamp"):
         sign_agent_token_request(private_jwk, agent_id="agent-1", 
audience="hub", timestamp=0)
+
+
[email protected](
+    ("field", "value"),
+    [
+        ("audience", "高德地图"),
+        ("agent_id", "agent-高德"),
+        ("kid", "kid-高德"),
+    ],
+)
+def 
test_signing_rejects_non_ascii_canonical_component_without_leaking_encoder_error(field,
 value):
+    private_jwk, _ = generate_agent_key_pair(value if field == "kid" else 
"key-1")
+    kwargs = {"agent_id": "agent-1", "audience": "hub", "timestamp": 100}
+    if field != "kid":
+        kwargs[field] = value
+    with pytest.raises(InvalidParameter) as raised:
+        sign_agent_token_request(private_jwk, **kwargs)
+    error = raised.value
+    assert error.error_code == "E3021"
+    assert f"{field} must contain only ASCII characters" in str(error)
+    assert "UnicodeEncodeError" not in str(error)
+    assert error.__cause__ is None
+
+
[email protected]("field", ["agent_id", "audience", "kid"])
+def test_signing_rejects_canonical_message_delimiter(field):
+    private_jwk, _ = generate_agent_key_pair("key|1" if field == "kid" else 
"key-1")
+    kwargs = {"agent_id": "agent-1", "audience": "hub", "timestamp": 100}
+    if field != "kid":
+        kwargs[field] = "value|break"
+    with pytest.raises(InvalidParameter, match="must not contain '\\|'") as 
raised:
+        sign_agent_token_request(private_jwk, **kwargs)
+    assert raised.value.error_code == "E3021"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/modelscope_hub-0.4.2/tests/test_compat_constants_completeness.py 
new/modelscope_hub-0.4.3/tests/test_compat_constants_completeness.py
--- old/modelscope_hub-0.4.2/tests/test_compat_constants_completeness.py        
1970-01-01 01:00:00.000000000 +0100
+++ new/modelscope_hub-0.4.3/tests/test_compat_constants_completeness.py        
2026-09-15 14:09:12.688884000 +0200
@@ -0,0 +1,88 @@
+"""Completeness / consistency of the legacy compat constant surface.
+
+``modelscope.hub.constants`` re-exports these names from
+``modelscope_hub.compat.constants``, which in turn re-exports them from the 
root
+``modelscope_hub.constants`` (the single source of truth). Two invariants must
+hold and are pinned below:
+
+* every legacy constant is present in the compat surface with the exact legacy
+  value and type (what the modelscope SDK imports); and
+* the root module is a superset of the compat surface for the shared prefixes
+  (so a legacy alias is never defined only in the compat shim again).
+"""
+
+from __future__ import annotations
+
+import os
+from pathlib import Path
+
+import pytest
+
+import modelscope_hub.compat.constants as hub_mod
+
+# Exact values (and types) as historically defined in modelscope.hub.constants.
+_EXPECTED: dict[str, object] = {
+    "MODEL_ID_SEPARATOR": "/",
+    "DEFAULT_MODELSCOPE_GROUP": "damo",
+    "DEFAULT_MODELSCOPE_DOMAIN": "www.modelscope.cn",
+    "DEFAULT_MODELSCOPE_INTL_DOMAIN": "www.modelscope.ai",
+    "DEFAULT_MODELSCOPE_DATA_ENDPOINT": "https://www.modelscope.cn";,
+    "DEFAULT_MODELSCOPE_INTL_DATA_ENDPOINT": "https://www.modelscope.ai";,
+    "DEFAULT_SKILLS_DIR": os.path.join(os.path.expanduser("~"), ".agents", 
"skills"),
+    "DEFAULT_CREDENTIALS_PATH": Path.home().joinpath(".modelscope", 
"credentials"),
+}
+
+
[email protected](("name", "expected"), sorted(_EXPECTED.items()))
+def test_compat_exposes_legacy_constant(name, expected):
+    assert hasattr(hub_mod, name), f"{name} missing from 
modelscope_hub.compat.constants"
+    value = getattr(hub_mod, name)
+    assert value == expected
+    # Type must match too: modelscope derives MODELSCOPE_CREDENTIALS_PATH via
+    # DEFAULT_CREDENTIALS_PATH.as_posix(), so a str stand-in would break it.
+    assert type(value) is type(expected)
+
+
+def test_credentials_path_is_a_path_supporting_as_posix():
+    assert isinstance(hub_mod.DEFAULT_CREDENTIALS_PATH, Path)
+    assert 
hub_mod.DEFAULT_CREDENTIALS_PATH.as_posix().endswith("/.modelscope/credentials")
+
+
+def test_matches_installed_modelscope_when_available():
+    """Mirror the field completeness/consistency check against the real SDK.
+
+    Skipped when ``modelscope`` is not importable (e.g. the hub CI matrix),
+    so the pinned assertions above remain the source of truth there.
+    """
+    legacy = pytest.importorskip("modelscope.hub.constants")
+    prefixes = ("UPLOAD_", "REPO_", "MODEL_", "DEFAULT_", "TEMPORARY_", 
"FILE_")
+    targets = [a for a in dir(legacy) if not a.startswith("_") and 
a.startswith(prefixes)]
+    missing = [a for a in targets if not hasattr(hub_mod, a)]
+    mismatch = [
+        (a, getattr(legacy, a), getattr(hub_mod, a))
+        for a in targets
+        if hasattr(hub_mod, a) and getattr(legacy, a) != getattr(hub_mod, a)
+    ]
+    assert not missing, f"missing from compat: {missing}"
+    assert not mismatch, f"value mismatch: {mismatch}"
+
+
+def test_root_module_is_superset_of_compat_surface():
+    """``modelscope_hub.constants`` must expose every prefixed compat constant.
+
+    compat re-exports from the root module, so the root is the single source of
+    truth; a name present in compat but missing from (or unequal in) the root
+    means a legacy alias regressed into being compat-only again.
+    """
+    import modelscope_hub.constants as root_mod
+
+    prefixes = ("UPLOAD_", "REPO_", "MODEL_", "DEFAULT_", "TEMPORARY_", 
"FILE_", "MCP_")
+    keys = [a for a in dir(hub_mod) if not a.startswith("_") and 
a.startswith(prefixes)]
+    missing = [k for k in keys if not hasattr(root_mod, k)]
+    mismatch = [
+        (k, getattr(hub_mod, k), getattr(root_mod, k))
+        for k in keys
+        if hasattr(root_mod, k) and getattr(hub_mod, k) != getattr(root_mod, k)
+    ]
+    assert not missing, f"missing from modelscope_hub.constants: {missing}"
+    assert not mismatch, f"value mismatch: {mismatch}"

Reply via email to