modelscope-hub 0.4.2__tar.gz → 0.4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {modelscope_hub-0.4.2/src/modelscope_hub.egg-info → modelscope_hub-0.4.3}/PKG-INFO +1 -1
  2. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_openapi.py +66 -2
  3. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/agent/_api.py +90 -6
  4. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/agent_idp.py +35 -3
  5. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/api.py +15 -11
  6. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/agent.py +16 -0
  7. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/mcp.py +1 -2
  8. modelscope_hub-0.4.3/src/modelscope_hub/compat/constants.py +84 -0
  9. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/constants.py +54 -0
  10. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/version.py +1 -1
  11. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3/src/modelscope_hub.egg-info}/PKG-INFO +1 -1
  12. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub.egg-info/SOURCES.txt +1 -0
  13. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_agent_idp.py +15 -0
  14. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_agent_idp_keys.py +33 -0
  15. modelscope_hub-0.4.3/tests/test_compat_constants_completeness.py +88 -0
  16. modelscope_hub-0.4.2/src/modelscope_hub/compat/constants.py +0 -87
  17. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/LICENSE +0 -0
  18. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/README.md +0 -0
  19. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/pyproject.toml +0 -0
  20. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/setup.cfg +0 -0
  21. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/__init__.py +0 -0
  22. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_cache_manager.py +0 -0
  23. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_download.py +0 -0
  24. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_git.py +0 -0
  25. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_legacy_api.py +0 -0
  26. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_repository.py +0 -0
  27. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_upload.py +0 -0
  28. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/agent/__init__.py +0 -0
  29. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/__init__.py +0 -0
  30. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/agent_idp.py +0 -0
  31. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/base.py +0 -0
  32. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/cache.py +0 -0
  33. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/compat.py +0 -0
  34. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/deploy.py +0 -0
  35. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/download.py +0 -0
  36. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/login.py +0 -0
  37. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/main.py +0 -0
  38. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/repo.py +0 -0
  39. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/secret.py +0 -0
  40. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/studio.py +0 -0
  41. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/upload.py +0 -0
  42. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/compat/__init__.py +0 -0
  43. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/compat/file_download.py +0 -0
  44. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/compat/hub_api.py +0 -0
  45. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/compat/snapshot_download.py +0 -0
  46. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/config.py +0 -0
  47. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/errors.py +0 -0
  48. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/py.typed +0 -0
  49. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/types.py +0 -0
  50. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/__init__.py +0 -0
  51. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/file_utils.py +0 -0
  52. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/format.py +0 -0
  53. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/logger.py +0 -0
  54. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/media.py +0 -0
  55. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/patterns.py +0 -0
  56. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/time_utils.py +0 -0
  57. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub.egg-info/dependency_links.txt +0 -0
  58. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub.egg-info/entry_points.txt +0 -0
  59. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub.egg-info/requires.txt +0 -0
  60. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub.egg-info/top_level.txt +0 -0
  61. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_cache_verification.py +0 -0
  62. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_compat_delete_files.py +0 -0
  63. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_compat_get_model_files.py +0 -0
  64. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_compat_snapshot_download.py +0 -0
  65. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_compat_studio.py +0 -0
  66. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_config_token.py +0 -0
  67. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_credential_lifecycle.py +0 -0
  68. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_download_lock_path.py +0 -0
  69. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_errors_openapi_codes.py +0 -0
  70. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_legacy_cache_detection.py +0 -0
  71. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_login_failure_paths.py +0 -0
  72. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_login_scoped_token.py +0 -0
  73. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_openapi_coverage.py +0 -0
  74. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_repo_files_truncation.py +0 -0
  75. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_upload_config.py +0 -0
  76. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_upload_lfs_gate.py +0 -0
  77. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_user_info.py +0 -0
  78. {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelscope-hub
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: The official Python client to connect with ModelScope Hub.
5
5
  License: Apache-2.0
6
6
  Keywords: modelscope,hub,sdk,openapi,machine-learning
@@ -1084,6 +1084,62 @@ class OpenAPIClient:
1084
1084
  params.append((key, str(value).lower() if isinstance(value, bool) else str(value)))
1085
1085
  return params
1086
1086
 
1087
+ @staticmethod
1088
+ def _decode_mcp_list_response(response: requests.Response) -> JSON:
1089
+ """Decode MCP list replies, including legacy HTTP-200 error envelopes.
1090
+
1091
+ The service normally signals errors through HTTP status codes. Some
1092
+ deployments instead return ``200`` with ``success: false`` and an
1093
+ OpenAPI-style error code. Treating that dictionary as a list payload
1094
+ silently turned invalid request bodies into empty server lists.
1095
+ """
1096
+ try:
1097
+ payload = response.json()
1098
+ except ValueError:
1099
+ return {"raw_response": response.text}
1100
+ if not isinstance(payload, dict):
1101
+ return payload
1102
+
1103
+ success = payload.get("success") if "success" in payload else payload.get("Success")
1104
+ if success is False:
1105
+ code = payload.get("code") if payload.get("code") is not None else payload.get("Code")
1106
+ message = next(
1107
+ (
1108
+ value.strip()
1109
+ for key in ("message", "Message", "msg", "Msg", "detail", "Detail")
1110
+ if isinstance(value := payload.get(key), str) and value.strip()
1111
+ ),
1112
+ "MCP server list request was rejected.",
1113
+ )
1114
+ request_id = payload.get("request_id") or payload.get("requestId") or payload.get("RequestId")
1115
+ code_text = str(code).strip()
1116
+ error_cls: type[APIError] = APIError
1117
+ status_code = 400
1118
+ if code_text == "InputParameterError":
1119
+ error_cls = InvalidParameter
1120
+ elif code_text == "InvalidAuthentication":
1121
+ error_cls = AuthenticationError
1122
+ status_code = 401
1123
+ elif code_text == "OperationNotAllowed":
1124
+ error_cls = PermissionDeniedError
1125
+ status_code = 403
1126
+ elif code_text == "RateLimitExceed":
1127
+ error_cls = RateLimitError
1128
+ status_code = 429
1129
+ raise error_cls(
1130
+ message,
1131
+ status_code=status_code,
1132
+ request_id=request_id,
1133
+ response_body=payload,
1134
+ url=response.url,
1135
+ method=response.request.method if response.request else "PUT",
1136
+ )
1137
+ if "data" in payload:
1138
+ return payload["data"]
1139
+ if "Data" in payload:
1140
+ return payload["Data"]
1141
+ return payload
1142
+
1087
1143
  @staticmethod
1088
1144
  def _is_method_or_route_unsupported(exc: APIError) -> bool:
1089
1145
  return exc.status_code in (404, 405, 501)
@@ -1307,6 +1363,10 @@ class OpenAPIClient:
1307
1363
  filter : dict, optional
1308
1364
  Nested filter object. Supported keys: ``category``, ``is_hosted``.
1309
1365
  """
1366
+ if isinstance(page_number, bool) or not isinstance(page_number, int) or page_number < 1:
1367
+ raise InvalidParameter("page_number must be an integer >= 1.")
1368
+ if isinstance(page_size, bool) or not isinstance(page_size, int) or page_size < 1:
1369
+ raise InvalidParameter("page_size must be an integer >= 1.")
1310
1370
  if page_number * page_size > 100:
1311
1371
  # The service enforces this itself, answering 403 QuotaLimitExceed with
1312
1372
  # exactly this rule. Checking here spares the round trip and reports it
@@ -1326,14 +1386,16 @@ class OpenAPIClient:
1326
1386
  body = {k: v for k, v in body.items() if v is not None}
1327
1387
 
1328
1388
  def _get() -> JSON:
1329
- return self._request(
1389
+ response = self._request(
1330
1390
  "GET",
1331
1391
  "/mcp/servers",
1332
1392
  params=self._flatten_mcp_list_params(body),
1333
1393
  require_token=False,
1334
1394
  required_scope=TokenScope.READ,
1335
1395
  anonymous_retry=True,
1396
+ unwrap=False,
1336
1397
  )
1398
+ return self._decode_mcp_list_response(response)
1337
1399
 
1338
1400
  if self._mcp_list_supports_get:
1339
1401
  # This deployment already answered GET and refused PUT, so leading
@@ -1341,14 +1403,16 @@ class OpenAPIClient:
1341
1403
  return _get()
1342
1404
 
1343
1405
  try:
1344
- return self._request(
1406
+ response = self._request(
1345
1407
  "PUT",
1346
1408
  "/mcp/servers",
1347
1409
  json_body=body,
1348
1410
  require_token=False,
1349
1411
  required_scope=TokenScope.READ,
1350
1412
  anonymous_retry=True,
1413
+ unwrap=False,
1351
1414
  )
1415
+ return self._decode_mcp_list_response(response)
1352
1416
  except APIError as exc:
1353
1417
  if self._mcp_list_supports_get is False or not self._is_method_or_route_unsupported(exc):
1354
1418
  raise
@@ -22,8 +22,8 @@ from dataclasses import dataclass
22
22
 
23
23
  from .._openapi import OpenAPIClient
24
24
  from ..config import HubConfig
25
- from ..constants import Visibility
26
- from ..errors import AuthenticationError, NotExistError
25
+ from ..constants import TokenScope, Visibility
26
+ from ..errors import APIError, AuthenticationError, NotExistError, PermissionDeniedError
27
27
 
28
28
  logger = logging.getLogger("modelscope_hub.agent")
29
29
 
@@ -230,12 +230,88 @@ class AgentApi:
230
230
  """True if the repo exists, False on 404."""
231
231
  return self.repo_info(path, name) is not None
232
232
 
233
+ @staticmethod
234
+ def _decode_dolphin_list_response(response: object, *, list_url: str) -> object:
235
+ """Decode the legacy dolphin list envelope without hiding soft errors.
236
+
237
+ Most endpoints signal access denial with HTTP 403, which OpenAPIClient
238
+ maps normally. ``/api/v1/dolphin/agents`` can instead answer HTTP 200
239
+ with ``Code=OperationNotAllowed``. Its former decode path then treated
240
+ the payload as an empty result, making a scope error indistinguishable
241
+ from an owner with no repositories.
242
+ """
243
+ try:
244
+ payload = response.json() # type: ignore[union-attr]
245
+ except (AttributeError, ValueError) as exc:
246
+ raise APIError(
247
+ "Agent repository list endpoint returned a non-JSON response.",
248
+ status_code=500,
249
+ url=list_url,
250
+ method="PUT",
251
+ ) from exc
252
+
253
+ if not isinstance(payload, dict):
254
+ return payload
255
+
256
+ code = payload.get("Code") if payload.get("Code") is not None else payload.get("code")
257
+ success = payload.get("Success") if "Success" in payload else payload.get("success")
258
+ message = next(
259
+ (
260
+ value.strip()
261
+ for key in ("Message", "message", "Msg", "msg")
262
+ if isinstance(value := payload.get(key), str) and value.strip()
263
+ ),
264
+ "Agent repository list endpoint rejected the request.",
265
+ )
266
+ code_text = str(code).strip()
267
+ message_lower = message.lower()
268
+ permission_denied = (
269
+ code_text == "OperationNotAllowed"
270
+ or code_text == "403"
271
+ or "operationnotallowed" in message_lower
272
+ or "permission denied" in message_lower
273
+ or "not allowed" in message_lower
274
+ or "forbidden" in message_lower
275
+ )
276
+ failed_envelope = success is False or (code is not None and code_text not in ("", "0", "200"))
277
+ if permission_denied:
278
+ error = PermissionDeniedError(
279
+ f"OperationNotAllowed: {message}",
280
+ status_code=403,
281
+ request_id=payload.get("RequestId") or payload.get("request_id"),
282
+ response_body=payload,
283
+ url=list_url,
284
+ method="PUT",
285
+ )
286
+ error.suggestion = (
287
+ "Agent repository listing requires a token with 'read' permission; "
288
+ "an api-inference-only token cannot access this endpoint."
289
+ )
290
+ raise error
291
+ if failed_envelope:
292
+ raise APIError(
293
+ f"Agent repository list endpoint rejected the request: {message}",
294
+ status_code=400,
295
+ request_id=payload.get("RequestId") or payload.get("request_id"),
296
+ response_body=payload,
297
+ url=list_url,
298
+ method="PUT",
299
+ )
300
+
301
+ # Match OpenAPIClient's success-envelope unwrapping while preserving the
302
+ # outer envelope long enough to inspect soft errors above.
303
+ if "data" in payload:
304
+ return payload["data"]
305
+ if "Data" in payload:
306
+ return payload["Data"]
307
+ return payload
308
+
233
309
  def list_agents(self, owner: str | None = None, page_number: int = 1, page_size: int = 10) -> dict:
234
310
  """List agent repositories (PUT /api/v1/dolphin/agents).
235
311
 
236
- Queries the dolphin search endpoint. When *owner* is given it is sent as
237
- a ``Path contains`` criterion (the group filter). Returns a dict with
238
- 'items' (list of agent metadata dicts) and 'total_count' (int).
312
+ This is an account-scoped operation, not a public catalogue lookup. It
313
+ requires a token with ``read`` permission and must never downgrade a
314
+ denied request into an anonymous empty list.
239
315
  """
240
316
  criterion: list[dict] = []
241
317
  if owner:
@@ -254,7 +330,15 @@ class AgentApi:
254
330
  "Criterion": criterion,
255
331
  }
256
332
  list_url = f"{self.server}/api/v1/dolphin/agents"
257
- data = self._openapi.request("PUT", url=list_url, json_body=body, require_token=False)
333
+ response = self._openapi._request(
334
+ "PUT",
335
+ url=list_url,
336
+ json_body=body,
337
+ require_token=True,
338
+ required_scope=TokenScope.READ,
339
+ unwrap=False,
340
+ )
341
+ data = self._decode_dolphin_list_response(response, list_url=list_url)
258
342
  if isinstance(data, list):
259
343
  return {"items": data, "total_count": len(data)}
260
344
  if isinstance(data, dict):
@@ -54,6 +54,30 @@ def _decode_base64url(value: object, field: str) -> bytes:
54
54
  return decoded
55
55
 
56
56
 
57
+ def _canonical_signing_component(value: str, field: str) -> str:
58
+ """Validate one component of the server-defined ASCII signing message.
59
+
60
+ The Agent-IDP protocol signs the literal
61
+ ``agent_id|kid|audience|timestamp`` byte sequence. ``audience`` is the
62
+ target Hub application's ``client_id``, not a display name. Encoding a
63
+ Unicode display name as UTF-8 here would silently change the bytes the
64
+ service verifies, so reject it explicitly instead of leaking a Python
65
+ ``UnicodeEncodeError``.
66
+ """
67
+ try:
68
+ value.encode("ascii")
69
+ except UnicodeEncodeError:
70
+ reason = (
71
+ "the target Hub application's client_id"
72
+ if field == "audience"
73
+ else "the canonical Agent-IDP signature message"
74
+ )
75
+ raise InvalidParameter(f"{field} must contain only ASCII characters because it is {reason}.") from None
76
+ if "|" in value:
77
+ raise InvalidParameter(f"{field} must not contain '|', the Agent-IDP signature message delimiter.")
78
+ return value
79
+
80
+
57
81
  def _normalise_private_jwk(value: AgentJWK | Mapping[str, Any]) -> AgentJWK:
58
82
  """Validate an Ed25519 private JWK and return a safe structured copy."""
59
83
  if isinstance(value, AgentJWK):
@@ -186,20 +210,28 @@ def sign_agent_token_request(
186
210
  audience: str,
187
211
  timestamp: int,
188
212
  ) -> TokenSignPayload:
189
- """Build the signed body required by anonymous ``POST /agent_id/token``."""
213
+ """Build the signed body required by anonymous ``POST /agent_id/token``.
214
+
215
+ ``audience`` must be the ASCII Hub application ``client_id``. It is not a
216
+ human-readable application name: every component is signed as the literal
217
+ ASCII ``agent_id|kid|audience|timestamp`` protocol byte sequence.
218
+ """
190
219
  if not isinstance(agent_id, str) or not agent_id:
191
220
  raise InvalidParameter("agent_id must be a non-empty string.")
192
221
  if not isinstance(audience, str) or not audience:
193
222
  raise InvalidParameter("audience must be a non-empty string.")
194
223
  if isinstance(timestamp, bool) or not isinstance(timestamp, int) or timestamp <= 0:
195
224
  raise InvalidParameter("timestamp must be a positive Unix timestamp in seconds.")
225
+ agent_id = _canonical_signing_component(agent_id, "agent_id")
226
+ audience = _canonical_signing_component(audience, "audience")
196
227
  key = _normalise_private_jwk(private_jwk)
197
- message = f"{agent_id}|{key.kid}|{audience}|{timestamp}".encode("ascii")
228
+ kid = _canonical_signing_component(key.kid, "kid")
229
+ message = f"{agent_id}|{kid}|{audience}|{timestamp}".encode("ascii")
198
230
  private_bytes = _decode_base64url(key.d, "d")
199
231
  signature = Ed25519PrivateKey.from_private_bytes(private_bytes).sign(message)
200
232
  return {
201
233
  "agent_id": agent_id,
202
- "kid": key.kid,
234
+ "kid": kid,
203
235
  "audience": audience,
204
236
  "timestamp": timestamp,
205
237
  "signature": _encode_base64url(signature),
@@ -1707,11 +1707,9 @@ class HubApi:
1707
1707
  """
1708
1708
  rt = self._normalize_repo_type(repo_type)
1709
1709
  paths = self._normalize_delete_values(file_paths, "file_paths")
1710
- patterns = self._normalize_delete_values(
1711
- delete_patterns, "delete_patterns")
1710
+ patterns = self._normalize_delete_values(delete_patterns, "delete_patterns")
1712
1711
  if not paths and not patterns:
1713
- raise InvalidParameter(
1714
- "Provide at least one file path or delete pattern.")
1712
+ raise InvalidParameter("Provide at least one file path or delete pattern.")
1715
1713
 
1716
1714
  resolved_revision = revision or "master"
1717
1715
  if patterns:
@@ -1726,9 +1724,8 @@ class HubApi:
1726
1724
  if file.path and file.type != "tree"
1727
1725
  ]
1728
1726
  paths.extend(
1729
- path for path in remote_paths
1730
- if any(fnmatch.fnmatchcase(path, pattern)
1731
- for pattern in patterns))
1727
+ path for path in remote_paths if any(fnmatch.fnmatchcase(path, pattern) for pattern in patterns)
1728
+ )
1732
1729
 
1733
1730
  paths = list(dict.fromkeys(paths))
1734
1731
  if not paths:
@@ -1758,8 +1755,7 @@ class HubApi:
1758
1755
  normalized: list[str] = []
1759
1756
  for value in raw_values:
1760
1757
  if not isinstance(value, str):
1761
- raise InvalidParameter(
1762
- f"{parameter_name} must contain only strings.")
1758
+ raise InvalidParameter(f"{parameter_name} must contain only strings.")
1763
1759
  if value:
1764
1760
  normalized.append(value)
1765
1761
  return normalized
@@ -2240,8 +2236,16 @@ class HubApi:
2240
2236
  filter=filter,
2241
2237
  extra={k: v for k, v in extra.items() if v is not None} or None,
2242
2238
  )
2243
- items, total, page, size = self._extract_paged(payload)
2244
- return PagedResult(items=list(items), total_count=total, page_number=page, page_size=size)
2239
+ items, total, _page, _size = self._extract_paged(payload)
2240
+ # The MCP service currently omits page_number/page_size in its response.
2241
+ # The request values are authoritative, exactly as in list_repos("mcp"),
2242
+ # so callers can reliably tell which page they received.
2243
+ return PagedResult(
2244
+ items=list(items),
2245
+ total_count=total,
2246
+ page_number=page_number,
2247
+ page_size=page_size,
2248
+ )
2245
2249
 
2246
2250
  def list_operational_mcp_servers(self) -> PagedResult[dict]:
2247
2251
  """List the MCP servers the caller currently has hosted.
@@ -35,11 +35,27 @@ def _fail(message: str) -> int:
35
35
  return 1
36
36
 
37
37
 
38
+ def _is_operation_not_allowed(e: APIError) -> bool:
39
+ """Recognise the server's permission code in either message or envelope."""
40
+ if "OperationNotAllowed" in e.message:
41
+ return True
42
+ body = e.response_body
43
+ if not isinstance(body, dict):
44
+ return False
45
+ code = body.get("Code") if body.get("Code") is not None else body.get("code")
46
+ return str(code) == "OperationNotAllowed"
47
+
48
+
38
49
  def _api_error_message(e: APIError, action: str = "request") -> str:
39
50
  status = e.status_code or 0
40
51
  if status == 401:
41
52
  return "authentication failed. Please login again."
42
53
  if status == 403:
54
+ if action == "list" and _is_operation_not_allowed(e):
55
+ return (
56
+ "permission denied (403 OperationNotAllowed). Agent repository listing requires a token with "
57
+ "'read' permission; an api-inference-only token cannot access this endpoint."
58
+ )
43
59
  return "permission denied. You do not have access to this resource."
44
60
  if status == 404:
45
61
  return "resource not found. Check the repository name and try again."
@@ -64,12 +64,11 @@ class _McpList(CLICommand):
64
64
  (
65
65
  item.get("id") or item.get("Id") or "-",
66
66
  item.get("name") or item.get("Name") or "-",
67
- item.get("status") or item.get("Status") or "-",
68
67
  item.get("description") or item.get("Description") or "-",
69
68
  )
70
69
  for item in result.items
71
70
  ]
72
- info(render_table(rows, headers=["id", "name", "status", "description"]))
71
+ info(render_table(rows, headers=["id", "name", "description"]))
73
72
  info(f"\npage {result.page_number} / total {result.total_count}")
74
73
 
75
74
  @staticmethod
@@ -0,0 +1,84 @@
1
+ """Legacy constant mappings for backward compatibility with the modelscope SDK.
2
+
3
+ Every name here is re-exported from :mod:`modelscope_hub.constants`, the single
4
+ source of truth. This module defines no independent values of its own; it only
5
+ surfaces the historical names (plus a few integer / env-name aliases) that the
6
+ modelscope SDK imports.
7
+ """
8
+
9
+ from ..constants import ( # noqa: F401
10
+ DEFAULT_CREDENTIALS_PATH,
11
+ DEFAULT_DATASET_REVISION,
12
+ DEFAULT_MAX_WORKERS,
13
+ DEFAULT_MODELSCOPE_DATA_ENDPOINT,
14
+ DEFAULT_MODELSCOPE_DOMAIN,
15
+ DEFAULT_MODELSCOPE_GROUP,
16
+ DEFAULT_MODELSCOPE_INTL_DATA_ENDPOINT,
17
+ DEFAULT_MODELSCOPE_INTL_DOMAIN,
18
+ DEFAULT_SKILLS_DIR,
19
+ FILE_HASH,
20
+ MODEL_ID_SEPARATOR,
21
+ REPO_TYPE_DATASET,
22
+ REPO_TYPE_MODEL,
23
+ REPO_TYPE_STUDIO,
24
+ REPO_TYPE_SUPPORT,
25
+ TEMPORARY_FOLDER_NAME,
26
+ UPLOAD_ADAPTIVE_BATCH_SIZE,
27
+ UPLOAD_ADAPTIVE_BATCHING_ENABLED,
28
+ UPLOAD_BLOB_CONNECT_TIMEOUT,
29
+ UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS,
30
+ UPLOAD_BLOB_MAX_ATTEMPTS,
31
+ UPLOAD_BLOB_MAX_RETRIES,
32
+ UPLOAD_BLOB_PROGRESS_THRESHOLD_BYTES,
33
+ UPLOAD_BLOB_READ_TIMEOUT,
34
+ UPLOAD_BLOB_READ_TIMEOUT_SECONDS,
35
+ UPLOAD_BLOB_RETRY_BACKOFF,
36
+ UPLOAD_BLOB_RETRY_BACKOFF_BASE_SECONDS,
37
+ UPLOAD_BLOB_RETRY_MAX_DELAY_SECONDS,
38
+ UPLOAD_BLOB_RETRY_MAX_WAIT,
39
+ UPLOAD_BLOB_TIMEOUT,
40
+ UPLOAD_BLOB_TQDM_DISABLE_THRESHOLD,
41
+ UPLOAD_BLOB_VALIDATION_BATCH_MAX_OBJECTS,
42
+ UPLOAD_CACHE_ENABLED,
43
+ UPLOAD_COMMIT_BATCH_MAX_OPERATIONS,
44
+ UPLOAD_COMMIT_BATCH_SIZE,
45
+ UPLOAD_FAILED_FILE_MAX_RETRIES,
46
+ UPLOAD_FAILED_FILE_MAX_RETRY_ROUNDS,
47
+ UPLOAD_HTTP_RETRY_ALLOWED_METHODS,
48
+ UPLOAD_LFS_FORCE_THRESHOLD_BYTES,
49
+ UPLOAD_LFS_THRESHOLD,
50
+ UPLOAD_MAX_CONCURRENT_WORKERS,
51
+ UPLOAD_MAX_FILE_COUNT,
52
+ UPLOAD_MAX_FILE_COUNT_IN_DIR,
53
+ UPLOAD_MAX_FILE_SIZE,
54
+ UPLOAD_MAX_FILE_SIZE_BYTES,
55
+ UPLOAD_MAX_FILES_PER_DIRECTORY,
56
+ UPLOAD_NORMAL_FILE_SIZE_TOTAL_LIMIT,
57
+ UPLOAD_NORMAL_FILES_TOTAL_SIZE_BYTES,
58
+ UPLOAD_REACT_BACKOFF_MAX_EXPONENT,
59
+ UPLOAD_REACT_ENABLED,
60
+ UPLOAD_REACT_MAX_DELAY,
61
+ UPLOAD_REACT_ROUND2_BASE_DELAY,
62
+ UPLOAD_REACT_ROUND3_FILE_DELAY,
63
+ UPLOAD_RECOVERY_BACKOFF_MAX_EXPONENT,
64
+ UPLOAD_RECOVERY_ENABLED,
65
+ UPLOAD_RECOVERY_MAX_DELAY_SECONDS,
66
+ UPLOAD_RECOVERY_SERIAL_BACKOFF_BASE_SECONDS,
67
+ UPLOAD_RECOVERY_SINGLE_FILE_DELAY_SECONDS,
68
+ UPLOAD_RETRY_ALLOWED_METHODS,
69
+ UPLOAD_SIZE_THRESHOLD_TO_ENFORCE_LFS,
70
+ UPLOAD_USE_CACHE,
71
+ UPLOAD_VALIDATE_BLOB_BATCH_SIZE,
72
+ RepoType,
73
+ Visibility,
74
+ get_upload_ignore_file_pattern,
75
+ )
76
+
77
+ # Visibility integer constants matching the old SDK
78
+ ModelVisibility_PUBLIC: int = int(Visibility.PUBLIC)
79
+ ModelVisibility_PRIVATE: int = int(Visibility.PRIVATE)
80
+ ModelVisibility_INTERNAL: int = int(Visibility.INTERNAL)
81
+
82
+ # Env var name constants (old SDK imports these as string constants)
83
+ MODELSCOPE_DOMAIN: str = "MODELSCOPE_DOMAIN"
84
+ MODELSCOPE_PREFER_AI_SITE: str = "MODELSCOPE_PREFER_AI_SITE"
@@ -12,6 +12,7 @@ import sys
12
12
  import warnings
13
13
  from dataclasses import dataclass
14
14
  from enum import Enum, IntEnum
15
+ from pathlib import Path
15
16
 
16
17
  # ---------------------------------------------------------------------------
17
18
  # StrEnum compatibility shim (Python 3.10 lacks :class:`enum.StrEnum`).
@@ -182,6 +183,38 @@ OPENAPI_PREFIX: str = "/openapi/v1"
182
183
  LEGACY_API_PREFIX: str = "/api/v1"
183
184
 
184
185
 
186
+ # ---------------------------------------------------------------------------
187
+ # Repo-type string aliases and shared repo defaults.
188
+ #
189
+ # The modern surface models repo kinds as :class:`RepoType`; these string
190
+ # aliases and the dataset-revision default are the historical names the
191
+ # modelscope SDK consumes. Defined here so ``modelscope_hub.constants`` is the
192
+ # single source of truth (``compat.constants`` re-exports them).
193
+ # ---------------------------------------------------------------------------
194
+ REPO_TYPE_MODEL: str = RepoType.MODEL.value
195
+ REPO_TYPE_DATASET: str = RepoType.DATASET.value
196
+ REPO_TYPE_STUDIO: str = RepoType.STUDIO.value
197
+ REPO_TYPE_SUPPORT: list[str] = [REPO_TYPE_MODEL, REPO_TYPE_DATASET, REPO_TYPE_STUDIO]
198
+ DEFAULT_DATASET_REVISION: str = "master"
199
+
200
+ # ---------------------------------------------------------------------------
201
+ # Legacy modelscope domain / endpoint / filesystem constants.
202
+ #
203
+ # Historical names and shapes (``www.`` domains, the ``damo`` group, a ``Path``
204
+ # credentials location) the modelscope SDK consumes. The modern canonical
205
+ # endpoint stays :data:`DEFAULT_ENDPOINT` (no ``www``); these coexist for
206
+ # backward compatibility and ``compat.constants`` re-exports them.
207
+ # ---------------------------------------------------------------------------
208
+ MODEL_ID_SEPARATOR: str = "/"
209
+ DEFAULT_MODELSCOPE_GROUP: str = "damo"
210
+ DEFAULT_MODELSCOPE_DOMAIN: str = "www.modelscope.cn"
211
+ DEFAULT_MODELSCOPE_INTL_DOMAIN: str = "www.modelscope.ai"
212
+ DEFAULT_MODELSCOPE_DATA_ENDPOINT: str = "https://" + DEFAULT_MODELSCOPE_DOMAIN
213
+ DEFAULT_MODELSCOPE_INTL_DATA_ENDPOINT: str = "https://" + DEFAULT_MODELSCOPE_INTL_DOMAIN
214
+ DEFAULT_SKILLS_DIR: str = os.path.join(os.path.expanduser("~"), ".agents", "skills")
215
+ DEFAULT_CREDENTIALS_PATH: Path = Path.home().joinpath(".modelscope", "credentials")
216
+
217
+
185
218
  # ---------------------------------------------------------------------------
186
219
  # Helpers for environment-driven overrides (auto-registering)
187
220
  # ---------------------------------------------------------------------------
@@ -503,6 +536,9 @@ TEMPORARY_FOLDER_NAME: str = "._____temp"
503
536
  FILE_HASH_FIELD: str = "Sha256"
504
537
  """API response field name for file hash."""
505
538
 
539
+ FILE_HASH: str = FILE_HASH_FIELD
540
+ """Legacy alias of :data:`FILE_HASH_FIELD` (modelscope SDK name)."""
541
+
506
542
  ENV_FILE_LOCK: str = "MODELSCOPE_DOWNLOAD_FILE_LOCK"
507
543
  _env_register(
508
544
  ENV_FILE_LOCK,
@@ -755,6 +791,7 @@ UPLOAD_NORMAL_FILES_TOTAL_SIZE_BYTES: int = _env_int_mb_with_deprecated_units(
755
791
  # Deprecated Python aliases. Runtime code must use the explicit names above.
756
792
  UPLOAD_BLOB_CONNECT_TIMEOUT = UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS
757
793
  UPLOAD_BLOB_READ_TIMEOUT = UPLOAD_BLOB_READ_TIMEOUT_SECONDS
794
+ UPLOAD_BLOB_TIMEOUT = (UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS, UPLOAD_BLOB_READ_TIMEOUT_SECONDS)
758
795
  UPLOAD_BLOB_MAX_RETRIES = UPLOAD_BLOB_MAX_ATTEMPTS
759
796
  UPLOAD_BLOB_RETRY_BACKOFF = UPLOAD_BLOB_RETRY_BACKOFF_BASE_SECONDS
760
797
  UPLOAD_BLOB_RETRY_MAX_WAIT = UPLOAD_BLOB_RETRY_MAX_DELAY_SECONDS
@@ -775,6 +812,7 @@ UPLOAD_REACT_MAX_DELAY = UPLOAD_RECOVERY_MAX_DELAY_SECONDS
775
812
  DEFAULT_MAX_WORKERS = UPLOAD_MAX_CONCURRENT_WORKERS
776
813
  UPLOAD_USE_CACHE = UPLOAD_CACHE_ENABLED
777
814
  UPLOAD_LFS_ENFORCE_THRESHOLD = UPLOAD_LFS_FORCE_THRESHOLD_BYTES
815
+ UPLOAD_SIZE_THRESHOLD_TO_ENFORCE_LFS = UPLOAD_LFS_FORCE_THRESHOLD_BYTES
778
816
  UPLOAD_MAX_FILE_COUNT_IN_DIR = UPLOAD_MAX_FILES_PER_DIRECTORY
779
817
  UPLOAD_MAX_FILE_SIZE = UPLOAD_MAX_FILE_SIZE_BYTES
780
818
  UPLOAD_NORMAL_FILE_SIZE_TOTAL_LIMIT = UPLOAD_NORMAL_FILES_TOTAL_SIZE_BYTES
@@ -923,10 +961,18 @@ __all__ = [
923
961
  "CONFIG_DIR_NAME",
924
962
  "DATASET_LFS_SUFFIX",
925
963
  "DEFAULT_CACHE_DIR_NAME",
964
+ "DEFAULT_CREDENTIALS_PATH",
965
+ "DEFAULT_DATASET_REVISION",
926
966
  "DEFAULT_ENDPOINT",
927
967
  "DEFAULT_IGNORE_PATTERNS",
928
968
  "DEFAULT_INTL_ENDPOINT",
929
969
  "DEFAULT_MAX_WORKERS",
970
+ "DEFAULT_MODELSCOPE_DATA_ENDPOINT",
971
+ "DEFAULT_MODELSCOPE_DOMAIN",
972
+ "DEFAULT_MODELSCOPE_GROUP",
973
+ "DEFAULT_MODELSCOPE_INTL_DATA_ENDPOINT",
974
+ "DEFAULT_MODELSCOPE_INTL_DOMAIN",
975
+ "DEFAULT_SKILLS_DIR",
930
976
  "DOWNLOAD_CHUNK_SIZE",
931
977
  "DOWNLOAD_PARALLEL_THRESHOLD",
932
978
  "DOWNLOAD_PARALLELS",
@@ -942,12 +988,18 @@ __all__ = [
942
988
  "ENV_PREFER_AI_SITE",
943
989
  "ENV_REGISTRY",
944
990
  "EnvVar",
991
+ "FILE_HASH",
945
992
  "FILE_HASH_FIELD",
946
993
  "get_upload_ignore_file_pattern",
947
994
  "LEGACY_API_PREFIX",
948
995
  "License",
996
+ "MODEL_ID_SEPARATOR",
949
997
  "MODEL_LFS_SUFFIX",
950
998
  "OPENAPI_PREFIX",
999
+ "REPO_TYPE_DATASET",
1000
+ "REPO_TYPE_MODEL",
1001
+ "REPO_TYPE_STUDIO",
1002
+ "REPO_TYPE_SUPPORT",
951
1003
  "RepoType",
952
1004
  "StrEnum",
953
1005
  "SESSION_FILE_NAME",
@@ -968,6 +1020,7 @@ __all__ = [
968
1020
  "UPLOAD_BLOB_RETRY_BACKOFF_BASE_SECONDS",
969
1021
  "UPLOAD_BLOB_RETRY_MAX_DELAY_SECONDS",
970
1022
  "UPLOAD_BLOB_RETRY_MAX_WAIT",
1023
+ "UPLOAD_BLOB_TIMEOUT",
971
1024
  "UPLOAD_BLOB_TQDM_DISABLE_THRESHOLD",
972
1025
  "UPLOAD_BLOB_VALIDATION_BATCH_MAX_OBJECTS",
973
1026
  "UPLOAD_CACHE_ENABLED",
@@ -1005,6 +1058,7 @@ __all__ = [
1005
1058
  "UPLOAD_RECOVERY_SERIAL_BACKOFF_BASE_SECONDS",
1006
1059
  "UPLOAD_RECOVERY_SINGLE_FILE_DELAY_SECONDS",
1007
1060
  "UPLOAD_RETRY_ALLOWED_METHODS",
1061
+ "UPLOAD_SIZE_THRESHOLD_TO_ENFORCE_LFS",
1008
1062
  "UPLOAD_USE_CACHE",
1009
1063
  "UPLOAD_VALIDATE_BLOB_BATCH_SIZE",
1010
1064
  "Visibility",
@@ -1,3 +1,3 @@
1
1
  """Version information for modelscope_hub."""
2
2
 
3
- __version__ = "0.4.2"
3
+ __version__ = "0.4.3"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelscope-hub
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: The official Python client to connect with ModelScope Hub.
5
5
  License: Apache-2.0
6
6
  Keywords: modelscope,hub,sdk,openapi,machine-learning
@@ -55,6 +55,7 @@ src/modelscope_hub/utils/time_utils.py
55
55
  tests/test_agent_idp.py
56
56
  tests/test_agent_idp_keys.py
57
57
  tests/test_cache_verification.py
58
+ tests/test_compat_constants_completeness.py
58
59
  tests/test_compat_delete_files.py
59
60
  tests/test_compat_get_model_files.py
60
61
  tests/test_compat_snapshot_download.py
@@ -8,6 +8,7 @@ import pytest
8
8
  import requests
9
9
 
10
10
  from modelscope_hub._openapi import OpenAPIClient
11
+ from modelscope_hub.agent_idp import generate_agent_key_pair
11
12
  from modelscope_hub.api import HubApi
12
13
  from modelscope_hub.config import HubConfig
13
14
  from modelscope_hub.errors import InvalidParameter
@@ -145,3 +146,17 @@ class TestAgentIdpFacade:
145
146
  )
146
147
  assert isinstance(token, AgentToken)
147
148
  assert token.access_token == "jwt"
149
+
150
+ def test_private_key_facade_rejects_non_ascii_audience_before_http(self):
151
+ private_jwk, _ = generate_agent_key_pair("key-1")
152
+ api = HubApi(token="test-token")
153
+ api._openapi = MagicMock()
154
+ with pytest.raises(InvalidParameter, match="audience must contain only ASCII characters") as raised:
155
+ api.issue_agent_token_with_private_key(
156
+ private_jwk,
157
+ agent_id="agent_id:modelscope:agent_xxx",
158
+ audience="高德地图",
159
+ timestamp=1700000000,
160
+ )
161
+ assert raised.value.error_code == "E3021"
162
+ api._openapi.issue_agent_token.assert_not_called()
@@ -73,3 +73,36 @@ def test_signing_rejects_invalid_timestamp():
73
73
  private_jwk, _ = generate_agent_key_pair()
74
74
  with pytest.raises(InvalidParameter, match="timestamp"):
75
75
  sign_agent_token_request(private_jwk, agent_id="agent-1", audience="hub", timestamp=0)
76
+
77
+
78
+ @pytest.mark.parametrize(
79
+ ("field", "value"),
80
+ [
81
+ ("audience", "高德地图"),
82
+ ("agent_id", "agent-高德"),
83
+ ("kid", "kid-高德"),
84
+ ],
85
+ )
86
+ def test_signing_rejects_non_ascii_canonical_component_without_leaking_encoder_error(field, value):
87
+ private_jwk, _ = generate_agent_key_pair(value if field == "kid" else "key-1")
88
+ kwargs = {"agent_id": "agent-1", "audience": "hub", "timestamp": 100}
89
+ if field != "kid":
90
+ kwargs[field] = value
91
+ with pytest.raises(InvalidParameter) as raised:
92
+ sign_agent_token_request(private_jwk, **kwargs)
93
+ error = raised.value
94
+ assert error.error_code == "E3021"
95
+ assert f"{field} must contain only ASCII characters" in str(error)
96
+ assert "UnicodeEncodeError" not in str(error)
97
+ assert error.__cause__ is None
98
+
99
+
100
+ @pytest.mark.parametrize("field", ["agent_id", "audience", "kid"])
101
+ def test_signing_rejects_canonical_message_delimiter(field):
102
+ private_jwk, _ = generate_agent_key_pair("key|1" if field == "kid" else "key-1")
103
+ kwargs = {"agent_id": "agent-1", "audience": "hub", "timestamp": 100}
104
+ if field != "kid":
105
+ kwargs[field] = "value|break"
106
+ with pytest.raises(InvalidParameter, match="must not contain '\\|'") as raised:
107
+ sign_agent_token_request(private_jwk, **kwargs)
108
+ assert raised.value.error_code == "E3021"
@@ -0,0 +1,88 @@
1
+ """Completeness / consistency of the legacy compat constant surface.
2
+
3
+ ``modelscope.hub.constants`` re-exports these names from
4
+ ``modelscope_hub.compat.constants``, which in turn re-exports them from the root
5
+ ``modelscope_hub.constants`` (the single source of truth). Two invariants must
6
+ hold and are pinned below:
7
+
8
+ * every legacy constant is present in the compat surface with the exact legacy
9
+ value and type (what the modelscope SDK imports); and
10
+ * the root module is a superset of the compat surface for the shared prefixes
11
+ (so a legacy alias is never defined only in the compat shim again).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ from pathlib import Path
18
+
19
+ import pytest
20
+
21
+ import modelscope_hub.compat.constants as hub_mod
22
+
23
+ # Exact values (and types) as historically defined in modelscope.hub.constants.
24
+ _EXPECTED: dict[str, object] = {
25
+ "MODEL_ID_SEPARATOR": "/",
26
+ "DEFAULT_MODELSCOPE_GROUP": "damo",
27
+ "DEFAULT_MODELSCOPE_DOMAIN": "www.modelscope.cn",
28
+ "DEFAULT_MODELSCOPE_INTL_DOMAIN": "www.modelscope.ai",
29
+ "DEFAULT_MODELSCOPE_DATA_ENDPOINT": "https://www.modelscope.cn",
30
+ "DEFAULT_MODELSCOPE_INTL_DATA_ENDPOINT": "https://www.modelscope.ai",
31
+ "DEFAULT_SKILLS_DIR": os.path.join(os.path.expanduser("~"), ".agents", "skills"),
32
+ "DEFAULT_CREDENTIALS_PATH": Path.home().joinpath(".modelscope", "credentials"),
33
+ }
34
+
35
+
36
+ @pytest.mark.parametrize(("name", "expected"), sorted(_EXPECTED.items()))
37
+ def test_compat_exposes_legacy_constant(name, expected):
38
+ assert hasattr(hub_mod, name), f"{name} missing from modelscope_hub.compat.constants"
39
+ value = getattr(hub_mod, name)
40
+ assert value == expected
41
+ # Type must match too: modelscope derives MODELSCOPE_CREDENTIALS_PATH via
42
+ # DEFAULT_CREDENTIALS_PATH.as_posix(), so a str stand-in would break it.
43
+ assert type(value) is type(expected)
44
+
45
+
46
+ def test_credentials_path_is_a_path_supporting_as_posix():
47
+ assert isinstance(hub_mod.DEFAULT_CREDENTIALS_PATH, Path)
48
+ assert hub_mod.DEFAULT_CREDENTIALS_PATH.as_posix().endswith("/.modelscope/credentials")
49
+
50
+
51
+ def test_matches_installed_modelscope_when_available():
52
+ """Mirror the field completeness/consistency check against the real SDK.
53
+
54
+ Skipped when ``modelscope`` is not importable (e.g. the hub CI matrix),
55
+ so the pinned assertions above remain the source of truth there.
56
+ """
57
+ legacy = pytest.importorskip("modelscope.hub.constants")
58
+ prefixes = ("UPLOAD_", "REPO_", "MODEL_", "DEFAULT_", "TEMPORARY_", "FILE_")
59
+ targets = [a for a in dir(legacy) if not a.startswith("_") and a.startswith(prefixes)]
60
+ missing = [a for a in targets if not hasattr(hub_mod, a)]
61
+ mismatch = [
62
+ (a, getattr(legacy, a), getattr(hub_mod, a))
63
+ for a in targets
64
+ if hasattr(hub_mod, a) and getattr(legacy, a) != getattr(hub_mod, a)
65
+ ]
66
+ assert not missing, f"missing from compat: {missing}"
67
+ assert not mismatch, f"value mismatch: {mismatch}"
68
+
69
+
70
+ def test_root_module_is_superset_of_compat_surface():
71
+ """``modelscope_hub.constants`` must expose every prefixed compat constant.
72
+
73
+ compat re-exports from the root module, so the root is the single source of
74
+ truth; a name present in compat but missing from (or unequal in) the root
75
+ means a legacy alias regressed into being compat-only again.
76
+ """
77
+ import modelscope_hub.constants as root_mod
78
+
79
+ prefixes = ("UPLOAD_", "REPO_", "MODEL_", "DEFAULT_", "TEMPORARY_", "FILE_", "MCP_")
80
+ keys = [a for a in dir(hub_mod) if not a.startswith("_") and a.startswith(prefixes)]
81
+ missing = [k for k in keys if not hasattr(root_mod, k)]
82
+ mismatch = [
83
+ (k, getattr(hub_mod, k), getattr(root_mod, k))
84
+ for k in keys
85
+ if hasattr(root_mod, k) and getattr(hub_mod, k) != getattr(root_mod, k)
86
+ ]
87
+ assert not missing, f"missing from modelscope_hub.constants: {missing}"
88
+ assert not mismatch, f"value mismatch: {mismatch}"
@@ -1,87 +0,0 @@
1
- """Legacy constant mappings for backward compatibility with modelscope SDK."""
2
-
3
- from ..constants import (
4
- UPLOAD_ADAPTIVE_BATCHING_ENABLED,
5
- UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS,
6
- UPLOAD_BLOB_MAX_ATTEMPTS,
7
- UPLOAD_BLOB_PROGRESS_THRESHOLD_BYTES,
8
- UPLOAD_BLOB_READ_TIMEOUT_SECONDS,
9
- UPLOAD_BLOB_RETRY_BACKOFF_BASE_SECONDS,
10
- UPLOAD_BLOB_RETRY_MAX_DELAY_SECONDS,
11
- UPLOAD_BLOB_VALIDATION_BATCH_MAX_OBJECTS,
12
- UPLOAD_CACHE_ENABLED,
13
- UPLOAD_COMMIT_BATCH_MAX_OPERATIONS,
14
- UPLOAD_FAILED_FILE_MAX_RETRY_ROUNDS,
15
- UPLOAD_HTTP_RETRY_ALLOWED_METHODS,
16
- UPLOAD_LFS_FORCE_THRESHOLD_BYTES,
17
- UPLOAD_MAX_CONCURRENT_WORKERS,
18
- UPLOAD_MAX_FILE_SIZE_BYTES,
19
- UPLOAD_MAX_FILES_PER_DIRECTORY,
20
- UPLOAD_NORMAL_FILES_TOTAL_SIZE_BYTES,
21
- UPLOAD_RECOVERY_BACKOFF_MAX_EXPONENT,
22
- UPLOAD_RECOVERY_ENABLED,
23
- UPLOAD_RECOVERY_MAX_DELAY_SECONDS,
24
- UPLOAD_RECOVERY_SERIAL_BACKOFF_BASE_SECONDS,
25
- UPLOAD_RECOVERY_SINGLE_FILE_DELAY_SECONDS,
26
- RepoType,
27
- Visibility,
28
- get_upload_ignore_file_pattern, # noqa: F401
29
- )
30
- from ..constants import (
31
- UPLOAD_LFS_THRESHOLD as _UPLOAD_LFS_THRESHOLD,
32
- )
33
- from ..constants import (
34
- UPLOAD_MAX_FILE_COUNT as _UPLOAD_MAX_FILE_COUNT,
35
- )
36
-
37
- REPO_TYPE_MODEL: str = RepoType.MODEL.value
38
- REPO_TYPE_DATASET: str = RepoType.DATASET.value
39
- REPO_TYPE_STUDIO: str = RepoType.STUDIO.value
40
- REPO_TYPE_SUPPORT: list[str] = [REPO_TYPE_MODEL, REPO_TYPE_DATASET, REPO_TYPE_STUDIO]
41
-
42
- DEFAULT_DATASET_REVISION: str = "master"
43
- DEFAULT_MAX_WORKERS: int = UPLOAD_MAX_CONCURRENT_WORKERS
44
-
45
- # Legacy upload constants. Values come from the modelscope-hub runtime config.
46
- UPLOAD_BLOB_CONNECT_TIMEOUT: int = UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS
47
- UPLOAD_BLOB_READ_TIMEOUT: int = UPLOAD_BLOB_READ_TIMEOUT_SECONDS
48
- UPLOAD_BLOB_MAX_RETRIES: int = UPLOAD_BLOB_MAX_ATTEMPTS
49
- UPLOAD_BLOB_RETRY_BACKOFF: int = UPLOAD_BLOB_RETRY_BACKOFF_BASE_SECONDS
50
- UPLOAD_BLOB_RETRY_MAX_WAIT: int = UPLOAD_BLOB_RETRY_MAX_DELAY_SECONDS
51
- UPLOAD_FAILED_FILE_MAX_RETRIES: int = UPLOAD_FAILED_FILE_MAX_RETRY_ROUNDS
52
- UPLOAD_BLOB_TIMEOUT: tuple[int, int] = (
53
- UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS,
54
- UPLOAD_BLOB_READ_TIMEOUT_SECONDS,
55
- )
56
- UPLOAD_RETRY_ALLOWED_METHODS: frozenset[str] = UPLOAD_HTTP_RETRY_ALLOWED_METHODS
57
- UPLOAD_MAX_FILE_SIZE: int = UPLOAD_MAX_FILE_SIZE_BYTES
58
- UPLOAD_MAX_FILE_COUNT: int = _UPLOAD_MAX_FILE_COUNT
59
- UPLOAD_SIZE_THRESHOLD_TO_ENFORCE_LFS: int = UPLOAD_LFS_FORCE_THRESHOLD_BYTES
60
- UPLOAD_LFS_THRESHOLD: int = _UPLOAD_LFS_THRESHOLD
61
- UPLOAD_MAX_FILE_COUNT_IN_DIR: int = UPLOAD_MAX_FILES_PER_DIRECTORY
62
- UPLOAD_NORMAL_FILE_SIZE_TOTAL_LIMIT: int = UPLOAD_NORMAL_FILES_TOTAL_SIZE_BYTES
63
- UPLOAD_COMMIT_BATCH_SIZE: int = UPLOAD_COMMIT_BATCH_MAX_OPERATIONS
64
- UPLOAD_VALIDATE_BLOB_BATCH_SIZE: int = UPLOAD_BLOB_VALIDATION_BATCH_MAX_OBJECTS
65
- UPLOAD_ADAPTIVE_BATCH_SIZE: bool = UPLOAD_ADAPTIVE_BATCHING_ENABLED
66
- UPLOAD_REACT_ENABLED: bool = UPLOAD_RECOVERY_ENABLED
67
- UPLOAD_REACT_ROUND2_BASE_DELAY: int = UPLOAD_RECOVERY_SERIAL_BACKOFF_BASE_SECONDS
68
- UPLOAD_REACT_ROUND3_FILE_DELAY: int = UPLOAD_RECOVERY_SINGLE_FILE_DELAY_SECONDS
69
- UPLOAD_REACT_BACKOFF_MAX_EXPONENT: int = UPLOAD_RECOVERY_BACKOFF_MAX_EXPONENT
70
- UPLOAD_REACT_MAX_DELAY: int = UPLOAD_RECOVERY_MAX_DELAY_SECONDS
71
- UPLOAD_BLOB_TQDM_DISABLE_THRESHOLD: int = UPLOAD_BLOB_PROGRESS_THRESHOLD_BYTES
72
- UPLOAD_USE_CACHE: bool = UPLOAD_CACHE_ENABLED
73
-
74
- # Visibility integer constants matching the old SDK
75
- ModelVisibility_PUBLIC: int = int(Visibility.PUBLIC)
76
- ModelVisibility_PRIVATE: int = int(Visibility.PRIVATE)
77
- ModelVisibility_INTERNAL: int = int(Visibility.INTERNAL)
78
-
79
- # Env var name constants (old SDK imports these as string constants)
80
- MODELSCOPE_DOMAIN: str = "MODELSCOPE_DOMAIN"
81
- MODELSCOPE_PREFER_AI_SITE: str = "MODELSCOPE_PREFER_AI_SITE"
82
-
83
- # API response field names
84
- FILE_HASH: str = "Sha256"
85
-
86
- # Download temp folder name
87
- TEMPORARY_FOLDER_NAME: str = "._____temp"
File without changes
File without changes
File without changes