modelscope-hub 0.4.2__tar.gz → 0.4.4__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.4}/PKG-INFO +1 -1
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_legacy_api.py +86 -6
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_openapi.py +85 -3
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_upload.py +769 -88
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/agent/_api.py +90 -6
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/agent_idp.py +35 -3
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/api.py +32 -12
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/agent.py +16 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/mcp.py +1 -2
- modelscope_hub-0.4.4/src/modelscope_hub/compat/constants.py +84 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/constants.py +361 -10
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/version.py +1 -1
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4/src/modelscope_hub.egg-info}/PKG-INFO +1 -1
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/SOURCES.txt +5 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_agent_idp.py +15 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_agent_idp_keys.py +33 -0
- modelscope_hub-0.4.4/tests/test_commit_action_limit.py +110 -0
- modelscope_hub-0.4.4/tests/test_compat_constants_completeness.py +88 -0
- modelscope_hub-0.4.4/tests/test_repo_tree_page_retry.py +143 -0
- modelscope_hub-0.4.4/tests/test_upload_batching.py +885 -0
- modelscope_hub-0.4.4/tests/test_upload_blob_dedup.py +256 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_upload_config.py +140 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_upload_lfs_gate.py +7 -12
- modelscope_hub-0.4.2/src/modelscope_hub/compat/constants.py +0 -87
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/LICENSE +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/README.md +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/pyproject.toml +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/setup.cfg +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/__init__.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_cache_manager.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_download.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_git.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_repository.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/agent/__init__.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/__init__.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/agent_idp.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/base.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/cache.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/compat.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/deploy.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/download.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/login.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/main.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/repo.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/secret.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/studio.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/upload.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/__init__.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/file_download.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/hub_api.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/snapshot_download.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/config.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/errors.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/py.typed +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/types.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/__init__.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/file_utils.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/format.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/logger.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/media.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/patterns.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/time_utils.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/dependency_links.txt +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/entry_points.txt +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/requires.txt +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/top_level.txt +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_cache_verification.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_compat_delete_files.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_compat_get_model_files.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_compat_snapshot_download.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_compat_studio.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_config_token.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_credential_lifecycle.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_download_lock_path.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_errors_openapi_codes.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_legacy_cache_detection.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_login_failure_paths.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_login_scoped_token.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_openapi_coverage.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_repo_files_truncation.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_user_info.py +0 -0
- {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_utils.py +0 -0
|
@@ -14,6 +14,7 @@ needed for API calls.
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import time
|
|
17
18
|
import uuid
|
|
18
19
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
19
20
|
from typing import IO, Any, BinaryIO
|
|
@@ -24,18 +25,28 @@ from requests.adapters import HTTPAdapter, Retry
|
|
|
24
25
|
|
|
25
26
|
from .constants import (
|
|
26
27
|
API_CONNECT_TIMEOUT,
|
|
28
|
+
API_CONNECTION_POOL_MAXSIZE,
|
|
27
29
|
API_MAX_RETRIES,
|
|
28
30
|
API_TIMEOUT,
|
|
29
31
|
LEGACY_API_PREFIX,
|
|
30
32
|
REPO_FILES_TRUNCATION_LIMIT,
|
|
31
33
|
REPO_TREE_MAX_REQUESTS,
|
|
34
|
+
REPO_TREE_PAGE_MAX_ATTEMPTS,
|
|
35
|
+
REPO_TREE_PAGE_RETRY_MAX_DELAY_SECONDS,
|
|
32
36
|
REPO_TREE_WALK_WORKERS,
|
|
33
37
|
UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS,
|
|
34
38
|
UPLOAD_BLOB_READ_TIMEOUT_SECONDS,
|
|
35
39
|
UPLOAD_HTTP_RETRY_ALLOWED_METHODS,
|
|
36
40
|
RepoType,
|
|
37
41
|
)
|
|
38
|
-
from .errors import
|
|
42
|
+
from .errors import (
|
|
43
|
+
InvalidParameter,
|
|
44
|
+
NetworkError,
|
|
45
|
+
PermissionDeniedError,
|
|
46
|
+
RequestTimeoutError,
|
|
47
|
+
ServerError,
|
|
48
|
+
raise_for_status,
|
|
49
|
+
)
|
|
39
50
|
from .utils.logger import get_logger
|
|
40
51
|
|
|
41
52
|
logger = get_logger("legacy_api")
|
|
@@ -100,6 +111,10 @@ class LegacyClient:
|
|
|
100
111
|
self._endpoint = endpoint.rstrip("/")
|
|
101
112
|
self._timeout: int | tuple[int, int] = (API_CONNECT_TIMEOUT, timeout)
|
|
102
113
|
self._session_authenticated = False
|
|
114
|
+
# Set once any file-tree read succeeds. From then on a 403 on a tree
|
|
115
|
+
# request cannot be an authorization result, so it is retried instead of
|
|
116
|
+
# aborting an enumeration that spans hundreds of requests.
|
|
117
|
+
self._tree_reads_ok = False
|
|
103
118
|
|
|
104
119
|
self._session = requests.Session()
|
|
105
120
|
if user_agent:
|
|
@@ -110,7 +125,11 @@ class LegacyClient:
|
|
|
110
125
|
status_forcelist=[429, 500, 502, 503, 504],
|
|
111
126
|
allowed_methods=UPLOAD_HTTP_RETRY_ALLOWED_METHODS,
|
|
112
127
|
)
|
|
113
|
-
adapter = HTTPAdapter(
|
|
128
|
+
adapter = HTTPAdapter(
|
|
129
|
+
max_retries=retry,
|
|
130
|
+
pool_connections=API_CONNECTION_POOL_MAXSIZE,
|
|
131
|
+
pool_maxsize=API_CONNECTION_POOL_MAXSIZE,
|
|
132
|
+
)
|
|
114
133
|
self._session.mount("https://", adapter)
|
|
115
134
|
self._session.mount("http://", adapter)
|
|
116
135
|
|
|
@@ -394,7 +413,12 @@ class LegacyClient:
|
|
|
394
413
|
params["Root"] = root
|
|
395
414
|
|
|
396
415
|
suffix = "repo/tree" if _is_dataset(repo_type) else "repo/files"
|
|
397
|
-
resp = self.
|
|
416
|
+
resp = self._request_repo_tree(
|
|
417
|
+
f"{segment}/{repo_id}/{suffix}",
|
|
418
|
+
params,
|
|
419
|
+
repo_id=repo_id,
|
|
420
|
+
authorized=self._tree_reads_ok,
|
|
421
|
+
)
|
|
398
422
|
data = self._json_data(resp)
|
|
399
423
|
if isinstance(data, list):
|
|
400
424
|
return data
|
|
@@ -567,6 +591,13 @@ class LegacyClient:
|
|
|
567
591
|
Datasets can have millions of files, so this method pages through
|
|
568
592
|
``GET /api/v1/datasets/{repo_id}/repo/tree`` with
|
|
569
593
|
``PageNumber``/``PageSize`` params.
|
|
594
|
+
|
|
595
|
+
A page occasionally answers ``403 无权访问该数据集`` on a repository the
|
|
596
|
+
caller demonstrably can read. Once any page has succeeded the credential
|
|
597
|
+
is proven, so a later-page denial is a server-side hiccup rather than an
|
|
598
|
+
authorization result, and it is retried instead of discarding every page
|
|
599
|
+
collected so far -- a large listing spans hundreds of pages, which makes
|
|
600
|
+
hitting it near-certain.
|
|
570
601
|
"""
|
|
571
602
|
all_files: list[dict] = []
|
|
572
603
|
page_number = 1
|
|
@@ -579,10 +610,11 @@ class LegacyClient:
|
|
|
579
610
|
}
|
|
580
611
|
if root_path and root_path != "/":
|
|
581
612
|
params["Root"] = root_path
|
|
582
|
-
resp = self.
|
|
583
|
-
"GET",
|
|
613
|
+
resp = self._request_repo_tree(
|
|
584
614
|
f"datasets/{repo_id}/repo/tree",
|
|
585
|
-
params
|
|
615
|
+
params,
|
|
616
|
+
repo_id=repo_id,
|
|
617
|
+
authorized=self._tree_reads_ok,
|
|
586
618
|
)
|
|
587
619
|
data = self._json_data(resp)
|
|
588
620
|
if isinstance(data, list):
|
|
@@ -598,6 +630,54 @@ class LegacyClient:
|
|
|
598
630
|
page_number += 1
|
|
599
631
|
return all_files
|
|
600
632
|
|
|
633
|
+
def _request_repo_tree(
|
|
634
|
+
self,
|
|
635
|
+
path: str,
|
|
636
|
+
params: dict[str, Any],
|
|
637
|
+
*,
|
|
638
|
+
repo_id: str,
|
|
639
|
+
authorized: bool,
|
|
640
|
+
) -> Any:
|
|
641
|
+
"""Fetch one file-tree listing, retrying a spurious denial.
|
|
642
|
+
|
|
643
|
+
The server intermittently answers a tree request with ``403 无权访问该数据
|
|
644
|
+
集`` on a repository the caller has just read successfully -- observed on
|
|
645
|
+
both ``PageNumber``-paginated and ``Root``-scoped listings. Enumerating a
|
|
646
|
+
large repository takes hundreds of such requests, so at that scale a
|
|
647
|
+
single-request failure rate is effectively a guaranteed whole-listing
|
|
648
|
+
failure, and it discards every entry gathered so far.
|
|
649
|
+
|
|
650
|
+
``authorized`` means some tree read already succeeded on this client, so
|
|
651
|
+
the credential is proven and a denial cannot be an authorization result.
|
|
652
|
+
Until then a 403 is taken at face value, keeping a real permission error
|
|
653
|
+
fast and honest.
|
|
654
|
+
"""
|
|
655
|
+
last_error: PermissionDeniedError | None = None
|
|
656
|
+
for attempt in range(REPO_TREE_PAGE_MAX_ATTEMPTS):
|
|
657
|
+
try:
|
|
658
|
+
resp = self._request("GET", path, params=params)
|
|
659
|
+
except PermissionDeniedError as error:
|
|
660
|
+
if not authorized:
|
|
661
|
+
raise
|
|
662
|
+
last_error = error
|
|
663
|
+
if attempt < REPO_TREE_PAGE_MAX_ATTEMPTS - 1:
|
|
664
|
+
wait = min(2**attempt, REPO_TREE_PAGE_RETRY_MAX_DELAY_SECONDS)
|
|
665
|
+
logger.warning(
|
|
666
|
+
"Repo %s: tree listing (%s) denied on an already-authorized repo, retrying in %ds ...",
|
|
667
|
+
repo_id,
|
|
668
|
+
params.get("Root") or params.get("PageNumber") or "/",
|
|
669
|
+
wait,
|
|
670
|
+
)
|
|
671
|
+
time.sleep(wait)
|
|
672
|
+
continue
|
|
673
|
+
self._tree_reads_ok = True
|
|
674
|
+
return resp
|
|
675
|
+
raise NetworkError(
|
|
676
|
+
f"Repo {repo_id}: tree listing ({params.get('Root') or params.get('PageNumber') or '/'}) kept "
|
|
677
|
+
f"returning a denial after {REPO_TREE_PAGE_MAX_ATTEMPTS} attempts on an already-authorized "
|
|
678
|
+
f"repo: {last_error}"
|
|
679
|
+
) from last_error
|
|
680
|
+
|
|
601
681
|
# ------------------------------------------------------------------
|
|
602
682
|
# Revisions
|
|
603
683
|
# ------------------------------------------------------------------
|
|
@@ -30,9 +30,17 @@ from typing import Any, BinaryIO
|
|
|
30
30
|
from urllib.parse import urljoin, urlsplit
|
|
31
31
|
|
|
32
32
|
import requests
|
|
33
|
+
from requests.adapters import HTTPAdapter
|
|
33
34
|
|
|
34
35
|
from .config import HubConfig, get_default_config
|
|
35
|
-
from .constants import
|
|
36
|
+
from .constants import (
|
|
37
|
+
API_CONNECT_TIMEOUT,
|
|
38
|
+
API_CONNECTION_POOL_MAXSIZE,
|
|
39
|
+
API_MAX_RETRIES,
|
|
40
|
+
API_TIMEOUT,
|
|
41
|
+
OPENAPI_PREFIX,
|
|
42
|
+
TokenScope,
|
|
43
|
+
)
|
|
36
44
|
from .errors import (
|
|
37
45
|
APIError,
|
|
38
46
|
AuthenticationError,
|
|
@@ -200,6 +208,16 @@ class OpenAPIClient:
|
|
|
200
208
|
) -> None:
|
|
201
209
|
self._config = config or get_default_config()
|
|
202
210
|
self._session = session or requests.Session()
|
|
211
|
+
if session is None:
|
|
212
|
+
# A bare Session caps the pool at urllib3's default of 10, which is
|
|
213
|
+
# below the concurrency bulk transfers use; the excess connections
|
|
214
|
+
# are discarded and pay for a new TLS handshake on next use.
|
|
215
|
+
adapter = HTTPAdapter(
|
|
216
|
+
pool_connections=API_CONNECTION_POOL_MAXSIZE,
|
|
217
|
+
pool_maxsize=API_CONNECTION_POOL_MAXSIZE,
|
|
218
|
+
)
|
|
219
|
+
self._session.mount("https://", adapter)
|
|
220
|
+
self._session.mount("http://", adapter)
|
|
203
221
|
self._timeout: float | tuple[float, float] = (
|
|
204
222
|
float(timeout) if timeout is not None else (float(API_CONNECT_TIMEOUT), float(API_TIMEOUT))
|
|
205
223
|
)
|
|
@@ -1084,6 +1102,62 @@ class OpenAPIClient:
|
|
|
1084
1102
|
params.append((key, str(value).lower() if isinstance(value, bool) else str(value)))
|
|
1085
1103
|
return params
|
|
1086
1104
|
|
|
1105
|
+
@staticmethod
|
|
1106
|
+
def _decode_mcp_list_response(response: requests.Response) -> JSON:
|
|
1107
|
+
"""Decode MCP list replies, including legacy HTTP-200 error envelopes.
|
|
1108
|
+
|
|
1109
|
+
The service normally signals errors through HTTP status codes. Some
|
|
1110
|
+
deployments instead return ``200`` with ``success: false`` and an
|
|
1111
|
+
OpenAPI-style error code. Treating that dictionary as a list payload
|
|
1112
|
+
silently turned invalid request bodies into empty server lists.
|
|
1113
|
+
"""
|
|
1114
|
+
try:
|
|
1115
|
+
payload = response.json()
|
|
1116
|
+
except ValueError:
|
|
1117
|
+
return {"raw_response": response.text}
|
|
1118
|
+
if not isinstance(payload, dict):
|
|
1119
|
+
return payload
|
|
1120
|
+
|
|
1121
|
+
success = payload.get("success") if "success" in payload else payload.get("Success")
|
|
1122
|
+
if success is False:
|
|
1123
|
+
code = payload.get("code") if payload.get("code") is not None else payload.get("Code")
|
|
1124
|
+
message = next(
|
|
1125
|
+
(
|
|
1126
|
+
value.strip()
|
|
1127
|
+
for key in ("message", "Message", "msg", "Msg", "detail", "Detail")
|
|
1128
|
+
if isinstance(value := payload.get(key), str) and value.strip()
|
|
1129
|
+
),
|
|
1130
|
+
"MCP server list request was rejected.",
|
|
1131
|
+
)
|
|
1132
|
+
request_id = payload.get("request_id") or payload.get("requestId") or payload.get("RequestId")
|
|
1133
|
+
code_text = str(code).strip()
|
|
1134
|
+
error_cls: type[APIError] = APIError
|
|
1135
|
+
status_code = 400
|
|
1136
|
+
if code_text == "InputParameterError":
|
|
1137
|
+
error_cls = InvalidParameter
|
|
1138
|
+
elif code_text == "InvalidAuthentication":
|
|
1139
|
+
error_cls = AuthenticationError
|
|
1140
|
+
status_code = 401
|
|
1141
|
+
elif code_text == "OperationNotAllowed":
|
|
1142
|
+
error_cls = PermissionDeniedError
|
|
1143
|
+
status_code = 403
|
|
1144
|
+
elif code_text == "RateLimitExceed":
|
|
1145
|
+
error_cls = RateLimitError
|
|
1146
|
+
status_code = 429
|
|
1147
|
+
raise error_cls(
|
|
1148
|
+
message,
|
|
1149
|
+
status_code=status_code,
|
|
1150
|
+
request_id=request_id,
|
|
1151
|
+
response_body=payload,
|
|
1152
|
+
url=response.url,
|
|
1153
|
+
method=response.request.method if response.request else "PUT",
|
|
1154
|
+
)
|
|
1155
|
+
if "data" in payload:
|
|
1156
|
+
return payload["data"]
|
|
1157
|
+
if "Data" in payload:
|
|
1158
|
+
return payload["Data"]
|
|
1159
|
+
return payload
|
|
1160
|
+
|
|
1087
1161
|
@staticmethod
|
|
1088
1162
|
def _is_method_or_route_unsupported(exc: APIError) -> bool:
|
|
1089
1163
|
return exc.status_code in (404, 405, 501)
|
|
@@ -1307,6 +1381,10 @@ class OpenAPIClient:
|
|
|
1307
1381
|
filter : dict, optional
|
|
1308
1382
|
Nested filter object. Supported keys: ``category``, ``is_hosted``.
|
|
1309
1383
|
"""
|
|
1384
|
+
if isinstance(page_number, bool) or not isinstance(page_number, int) or page_number < 1:
|
|
1385
|
+
raise InvalidParameter("page_number must be an integer >= 1.")
|
|
1386
|
+
if isinstance(page_size, bool) or not isinstance(page_size, int) or page_size < 1:
|
|
1387
|
+
raise InvalidParameter("page_size must be an integer >= 1.")
|
|
1310
1388
|
if page_number * page_size > 100:
|
|
1311
1389
|
# The service enforces this itself, answering 403 QuotaLimitExceed with
|
|
1312
1390
|
# exactly this rule. Checking here spares the round trip and reports it
|
|
@@ -1326,14 +1404,16 @@ class OpenAPIClient:
|
|
|
1326
1404
|
body = {k: v for k, v in body.items() if v is not None}
|
|
1327
1405
|
|
|
1328
1406
|
def _get() -> JSON:
|
|
1329
|
-
|
|
1407
|
+
response = self._request(
|
|
1330
1408
|
"GET",
|
|
1331
1409
|
"/mcp/servers",
|
|
1332
1410
|
params=self._flatten_mcp_list_params(body),
|
|
1333
1411
|
require_token=False,
|
|
1334
1412
|
required_scope=TokenScope.READ,
|
|
1335
1413
|
anonymous_retry=True,
|
|
1414
|
+
unwrap=False,
|
|
1336
1415
|
)
|
|
1416
|
+
return self._decode_mcp_list_response(response)
|
|
1337
1417
|
|
|
1338
1418
|
if self._mcp_list_supports_get:
|
|
1339
1419
|
# This deployment already answered GET and refused PUT, so leading
|
|
@@ -1341,14 +1421,16 @@ class OpenAPIClient:
|
|
|
1341
1421
|
return _get()
|
|
1342
1422
|
|
|
1343
1423
|
try:
|
|
1344
|
-
|
|
1424
|
+
response = self._request(
|
|
1345
1425
|
"PUT",
|
|
1346
1426
|
"/mcp/servers",
|
|
1347
1427
|
json_body=body,
|
|
1348
1428
|
require_token=False,
|
|
1349
1429
|
required_scope=TokenScope.READ,
|
|
1350
1430
|
anonymous_retry=True,
|
|
1431
|
+
unwrap=False,
|
|
1351
1432
|
)
|
|
1433
|
+
return self._decode_mcp_list_response(response)
|
|
1352
1434
|
except APIError as exc:
|
|
1353
1435
|
if self._mcp_list_supports_get is False or not self._is_method_or_route_unsupported(exc):
|
|
1354
1436
|
raise
|