modelscope-hub 0.4.3__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.3/src/modelscope_hub.egg-info → modelscope_hub-0.4.4}/PKG-INFO +1 -1
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/_legacy_api.py +86 -6
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/_openapi.py +19 -1
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/_upload.py +769 -88
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/agent/_api.py +1 -1
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/api.py +17 -1
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/constants.py +307 -10
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/version.py +1 -1
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4/src/modelscope_hub.egg-info}/PKG-INFO +1 -1
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/SOURCES.txt +4 -0
- modelscope_hub-0.4.4/tests/test_commit_action_limit.py +110 -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.3 → modelscope_hub-0.4.4}/tests/test_upload_config.py +140 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_upload_lfs_gate.py +7 -12
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/LICENSE +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/README.md +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/pyproject.toml +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/setup.cfg +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/__init__.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/_cache_manager.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/_download.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/_git.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/_repository.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/agent/__init__.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/agent_idp.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/__init__.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/agent.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/agent_idp.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/base.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/cache.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/compat.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/deploy.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/download.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/login.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/main.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/mcp.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/repo.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/secret.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/studio.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/upload.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/__init__.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/constants.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/file_download.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/hub_api.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/snapshot_download.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/config.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/errors.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/py.typed +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/types.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/__init__.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/file_utils.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/format.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/logger.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/media.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/patterns.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/time_utils.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/dependency_links.txt +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/entry_points.txt +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/requires.txt +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/top_level.txt +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_agent_idp.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_agent_idp_keys.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_cache_verification.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_compat_constants_completeness.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_compat_delete_files.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_compat_get_model_files.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_compat_snapshot_download.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_compat_studio.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_config_token.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_credential_lifecycle.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_download_lock_path.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_errors_openapi_codes.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_legacy_cache_detection.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_login_failure_paths.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_login_scoped_token.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_openapi_coverage.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_repo_files_truncation.py +0 -0
- {modelscope_hub-0.4.3 → modelscope_hub-0.4.4}/tests/test_user_info.py +0 -0
- {modelscope_hub-0.4.3 → 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
|
)
|