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.
- {modelscope_hub-0.4.2/src/modelscope_hub.egg-info → modelscope_hub-0.4.3}/PKG-INFO +1 -1
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_openapi.py +66 -2
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/agent/_api.py +90 -6
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/agent_idp.py +35 -3
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/api.py +15 -11
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/agent.py +16 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/mcp.py +1 -2
- modelscope_hub-0.4.3/src/modelscope_hub/compat/constants.py +84 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/constants.py +54 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/version.py +1 -1
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3/src/modelscope_hub.egg-info}/PKG-INFO +1 -1
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub.egg-info/SOURCES.txt +1 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_agent_idp.py +15 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_agent_idp_keys.py +33 -0
- modelscope_hub-0.4.3/tests/test_compat_constants_completeness.py +88 -0
- modelscope_hub-0.4.2/src/modelscope_hub/compat/constants.py +0 -87
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/LICENSE +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/README.md +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/pyproject.toml +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/setup.cfg +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/__init__.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_cache_manager.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_download.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_git.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_legacy_api.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_repository.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/_upload.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/agent/__init__.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/__init__.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/agent_idp.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/base.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/cache.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/compat.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/deploy.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/download.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/login.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/main.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/repo.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/secret.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/studio.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/cli/upload.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/compat/__init__.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/compat/file_download.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/compat/hub_api.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/compat/snapshot_download.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/config.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/errors.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/py.typed +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/types.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/__init__.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/file_utils.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/format.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/logger.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/media.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/patterns.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/utils/time_utils.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub.egg-info/dependency_links.txt +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub.egg-info/entry_points.txt +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub.egg-info/requires.txt +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub.egg-info/top_level.txt +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_cache_verification.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_compat_delete_files.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_compat_get_model_files.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_compat_snapshot_download.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_compat_studio.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_config_token.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_credential_lifecycle.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_download_lock_path.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_errors_openapi_codes.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_legacy_cache_detection.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_login_failure_paths.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_login_scoped_token.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_openapi_coverage.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_repo_files_truncation.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_upload_config.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_upload_lfs_gate.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_user_info.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/tests/test_utils.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
a
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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,
|
|
2244
|
-
|
|
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", "
|
|
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",
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub/compat/snapshot_download.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modelscope_hub-0.4.2 → modelscope_hub-0.4.3}/src/modelscope_hub.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|