Skip to content

Commit e4ca1e4

Browse files
Merge pull request #27 from modelscope/fix/v9_reviews
[Fix] Fix review issues
2 parents be16f3b + 8b1347e commit e4ca1e4

8 files changed

Lines changed: 258 additions & 23 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ build-backend = "setuptools.build_meta"
4545
[tool.setuptools.packages.find]
4646
where = ["src"]
4747

48+
[tool.setuptools.package-data]
49+
modelscope_hub = ["py.typed"]
50+
4851
[tool.setuptools.dynamic]
4952
version = {attr = "modelscope_hub.version.__version__"}
5053

src/modelscope_hub/_cache_manager.py

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,40 @@ def scan_cache(cache_dir: Path | None = None) -> CacheInfo:
8686
local_path=str(repo_dir),
8787
))
8888

89+
# Scan flat layout (compat): {root}/{owner}/{name}/
90+
# and legacy layout: {root}/hub/{owner}/{name}/
91+
_scanned_paths = {r.local_path for r in repos} # avoid double-counting
92+
93+
for prefix, prefix_path in [("", root), ("hub", root / "hub")]:
94+
if not prefix_path.is_dir():
95+
continue
96+
for owner_dir in prefix_path.iterdir():
97+
if not owner_dir.is_dir():
98+
continue
99+
# Skip known non-repo directories
100+
if owner_dir.name in ("hub", "models", "datasets", "studios", "mcps", "skills"):
101+
continue
102+
for name_dir in owner_dir.iterdir():
103+
if not name_dir.is_dir():
104+
continue
105+
if str(name_dir) in _scanned_paths:
106+
continue
107+
108+
size = _dir_size(name_dir)
109+
total_size += size
110+
nb_files = sum(1 for f in name_dir.rglob("*") if f.is_file())
111+
repo_id = f"{owner_dir.name}/{name_dir.name}"
112+
113+
repos.append(CachedRepoInfo(
114+
repo_id=repo_id,
115+
repo_type=RepoType.MODEL, # assume model for flat layout
116+
revision=None,
117+
size_on_disk=size,
118+
nb_files=nb_files,
119+
last_accessed=None,
120+
local_path=str(name_dir),
121+
))
122+
89123
return CacheInfo(
90124
repos=repos,
91125
total_size=total_size,
@@ -135,30 +169,42 @@ def clear_cache(
135169
freed = 0
136170

137171
if repo_id and repo_type:
138-
# Clear specific repo
139-
segment = f"{repo_type}s" if not repo_type.endswith("s") else repo_type
140-
safe_id = repo_id.replace("/", "--")
141-
target = root / segment / safe_id
142-
if target.is_dir():
143-
freed = _dir_size(target)
172+
# Clear specific repo — check all possible layout locations
173+
targets = _resolve_cache_targets(root, repo_id, repo_type)
174+
for target in targets:
175+
size = _dir_size(target)
176+
freed += size
144177
_safe_rmtree(target)
145-
logger.info("Cleared cache for %s/%s (%d bytes)", repo_type, repo_id, freed)
178+
logger.info("Cleared cache at %s (%d bytes)", target, size)
146179
elif repo_type:
147-
# Clear all repos of this type
180+
# Clear all repos of this type (standard layout)
148181
segment = f"{repo_type}s" if not repo_type.endswith("s") else repo_type
149182
type_dir = root / segment
150183
if type_dir.is_dir():
151-
freed = _dir_size(type_dir)
184+
size = _dir_size(type_dir)
185+
freed += size
152186
_safe_rmtree(type_dir)
153-
logger.info("Cleared all %s caches (%d bytes)", repo_type, freed)
187+
logger.info("Cleared %s standard cache (%d bytes)", repo_type, size)
188+
# Also clear legacy hub layout
189+
hub_dir = root / "hub"
190+
if hub_dir.is_dir():
191+
size = _dir_size(hub_dir)
192+
freed += size
193+
_safe_rmtree(hub_dir)
194+
logger.info("Cleared legacy hub cache (%d bytes)", size)
154195
else:
155-
# Clear everything
196+
# Clear everything (standard + legacy layouts)
156197
for repo_t in _DEFAULT_SCAN_TYPES:
157198
segment = f"{repo_t}s"
158199
type_dir = root / segment
159200
if type_dir.is_dir():
160201
freed += _dir_size(type_dir)
161202
_safe_rmtree(type_dir)
203+
# Legacy hub directory
204+
hub_dir = root / "hub"
205+
if hub_dir.is_dir():
206+
freed += _dir_size(hub_dir)
207+
_safe_rmtree(hub_dir)
162208
logger.info("Cleared all caches (%d bytes)", freed)
163209

164210
return freed
@@ -167,6 +213,25 @@ def clear_cache(
167213
# ---------------------------------------------------------------------------
168214
# Helpers
169215
# ---------------------------------------------------------------------------
216+
def _resolve_cache_targets(root: Path, repo_id: str, repo_type: str) -> list[Path]:
217+
"""Resolve all possible cache locations for a repo across layout formats.
218+
219+
Checks three path layouts:
220+
- Standard: {root}/{type}s/{owner}--{name}/
221+
- Flat: {root}/{owner}/{name}/
222+
- Legacy: {root}/hub/{owner}/{name}/
223+
"""
224+
safe_id = repo_id.replace("/", "--")
225+
segment = f"{repo_type}s" if not repo_type.endswith("s") else repo_type
226+
227+
candidates = [
228+
root / segment / safe_id, # standard: {cache}/models/owner--name/
229+
root / repo_id, # flat (compat): {cache}/owner/name/
230+
root / "hub" / repo_id, # legacy: {cache}/hub/owner/name/
231+
]
232+
return [p for p in candidates if p.is_dir()]
233+
234+
170235
def _dir_size(path: Path) -> int:
171236
"""Compute total size of all files under ``path`` recursively."""
172237
total = 0

src/modelscope_hub/api.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,10 @@ def _repo_info_from_payload(
331331
"Downloads": "downloads",
332332
"Likes": "likes",
333333
"CreatedAt": "created_at",
334-
"UpdatedAt": "updated_at",
334+
"UpdatedAt": "last_modified",
335+
"LastModified": "last_modified",
336+
"last_modified": "last_modified",
337+
"updated_at": "last_modified",
335338
"Tags": "tags",
336339
}
337340
for key, value in data.items():
@@ -341,8 +344,8 @@ def _repo_info_from_payload(
341344
# optional ``gated`` flag) instead of the legacy ``Visibility`` integer.
342345
# Translate it so downstream code sees a uniform ``Visibility`` enum.
343346
if normalised.get("visibility") is None:
344-
private_flag = normalised.pop("private", None)
345-
gated_flag = normalised.pop("gated", None)
347+
private_flag = normalised.get("private")
348+
gated_flag = normalised.get("gated")
346349
if isinstance(private_flag, bool):
347350
if private_flag:
348351
normalised["visibility"] = Visibility.PRIVATE
@@ -845,7 +848,15 @@ def list_repos(
845848
page = page_number
846849
size = page_size
847850
infos = [self._repo_info_from_payload(item, rt) for item in items]
848-
return PagedResult(items=infos, total_count=total, page_number=page, page_size=size)
851+
# Determine collection key for OpenAPI-aligned to_dict() output
852+
_COLLECTION_KEYS = {
853+
RepoType.MODEL: "models",
854+
RepoType.DATASET: "datasets",
855+
RepoType.SKILL: "skills",
856+
RepoType.MCP: "servers",
857+
}
858+
key = _COLLECTION_KEYS.get(rt, "items")
859+
return PagedResult(items=infos, total_count=total, page_number=page, page_size=size, collection_key=key)
849860

850861
def delete_repo(self, repo_id: str, repo_type: RepoTypeLike) -> None:
851862
"""Delete a repository.

src/modelscope_hub/cli/repo.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from pathlib import Path
1212

1313
from ..constants import RepoType
14-
from ..errors import is_repo_exists_error
14+
from ..errors import AlreadyExistsError, is_repo_exists_error
1515
from ..types import RepoInfo
1616
from .base import CLICommand, add_repo_type_arg, error, info, make_api, print_env_table, render_table, success
1717
from .compat import add_subcmd_token_endpoint
@@ -114,6 +114,11 @@ def execute(self) -> None:
114114
**extra,
115115
)
116116
success(f"Created {self.args.repo_type}: {repo.repo_id or self.args.repo_id}")
117+
except AlreadyExistsError:
118+
if getattr(self.args, "exist_ok", False):
119+
info(f"Repository already exists: {self.args.repo_id}")
120+
return
121+
raise
117122
except Exception as exc:
118123
if getattr(self.args, "exist_ok", False) and is_repo_exists_error(exc):
119124
info(f"Repository already exists: {self.args.repo_id}")

src/modelscope_hub/compat/hub_api.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ..api import HubApi
1818
from ..constants import RepoType
1919
from ..errors import (
20+
AlreadyExistsError,
2021
AuthenticationError,
2122
InvalidParameter,
2223
NotExistError,
@@ -26,7 +27,7 @@
2627
from ..utils.logger import get_logger
2728

2829
if TYPE_CHECKING:
29-
from ..types import RepoInfo
30+
from ..types import PagedResult, RepoInfo
3031

3132
logger = get_logger("compat")
3233

@@ -42,6 +43,9 @@ class LegacyHubApi:
4243
new HubApi implementation.
4344
"""
4445

46+
_api: HubApi
47+
_endpoint: str | None
48+
4549
def __init__(
4650
self,
4751
endpoint: str | None = None,
@@ -126,6 +130,10 @@ def create_repo(
126130
chinese_name=chinese_name,
127131
**kwargs,
128132
)
133+
except AlreadyExistsError:
134+
if exist_ok:
135+
return None
136+
raise
129137
except Exception as exc:
130138
if exist_ok and is_repo_exists_error(exc):
131139
return None
@@ -182,9 +190,12 @@ def push_model(self, model_id: str, model_dir: str, **kwargs: Any) -> None:
182190
license=kwargs.get("license"),
183191
chinese_name=kwargs.get("chinese_name"),
184192
)
193+
except AlreadyExistsError:
194+
logger.info("Repository '%s' already exists, proceeding with upload.", model_id)
185195
except Exception as exc:
186196
if not is_repo_exists_error(exc):
187197
raise
198+
logger.info("Repository '%s' already exists, proceeding with upload.", model_id)
188199
self._api.upload_folder(
189200
model_id,
190201
RepoType.MODEL,
@@ -248,6 +259,44 @@ def repo_exists(
248259
raise
249260
return False
250261

262+
def list_repos(
263+
self,
264+
repo_type: str | RepoType,
265+
*,
266+
owner: str | None = None,
267+
search: str | None = None,
268+
sort: str | None = None,
269+
page_number: int = 1,
270+
page_size: int = 10,
271+
**filters: Any,
272+
) -> "PagedResult[RepoInfo]":
273+
"""List repositories of the given type.
274+
275+
Delegates to :meth:`HubApi.list_repos`.
276+
"""
277+
return self._api.list_repos(
278+
repo_type,
279+
owner=owner,
280+
search=search,
281+
sort=sort,
282+
page_number=page_number,
283+
page_size=page_size,
284+
**filters,
285+
)
286+
287+
def get_repo(
288+
self,
289+
repo_id: str,
290+
repo_type: str | RepoType,
291+
*,
292+
revision: str | None = None,
293+
) -> "RepoInfo":
294+
"""Get repository information.
295+
296+
Delegates to :meth:`HubApi.get_repo`.
297+
"""
298+
return self._api.get_repo(repo_id, repo_type, revision=revision)
299+
251300
# ------------------------------------------------------------------
252301
# Download operations
253302
# ------------------------------------------------------------------
@@ -928,7 +977,8 @@ def dataset_download_statistics(
928977
"downloads": "Downloads",
929978
"likes": "Likes",
930979
"created_at": "CreatedAt",
931-
"updated_at": "UpdatedAt",
980+
"updated_at": "UpdatedAt", # backward compat if manually constructed
981+
"last_modified": "UpdatedAt",
932982
"license": "License",
933983
"tags": "Tags",
934984
}

src/modelscope_hub/errors.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,17 @@ class InvalidParameter(APIError, ValueError):
223223
suggestion = "Invalid request parameters. Please check and retry."
224224

225225

226+
class AlreadyExistsError(InvalidParameter):
227+
"""Resource already exists (e.g., repo name is taken).
228+
229+
Error code: E3026
230+
"""
231+
232+
error_code = "E3026"
233+
retryable = False
234+
suggestion = "Resource already exists. Use exist_ok=True to ignore."
235+
236+
226237
# -- Rate limiting (E1021) --------------------------------------------------
227238
class RateLimitError(APIError):
228239
"""Raised on HTTP 429 -- client should back off and retry later."""
@@ -454,6 +465,24 @@ def raise_for_status(response: "Response") -> None:
454465
else:
455466
exc_cls = _STATUS_MAP.get(status, APIError)
456467

468+
# Detect "already exists" errors before falling back to InvalidParameter
469+
if exc_cls is InvalidParameter and isinstance(body, dict):
470+
code = body.get("Code") or body.get("code")
471+
msg_text = (body.get("Message") or body.get("message")
472+
or body.get("msg") or body.get("Msg") or "").lower()
473+
is_exists = False
474+
if code is not None:
475+
try:
476+
if int(code) in _ALREADY_EXISTS_CODES:
477+
is_exists = True
478+
except (TypeError, ValueError):
479+
pass
480+
if not is_exists:
481+
if any(kw in msg_text for kw in _ALREADY_EXISTS_KEYWORDS):
482+
is_exists = True
483+
if is_exists:
484+
exc_cls = AlreadyExistsError
485+
457486
kwargs: dict[str, Any] = dict(
458487
status_code=status,
459488
request_id=request_id,
@@ -481,13 +510,36 @@ def raise_for_status(response: "Response") -> None:
481510
# ---------------------------------------------------------------------------
482511
# Repo-exists detection (shared by cli/repo.py and compat/hub_api.py)
483512
# ---------------------------------------------------------------------------
484-
_ALREADY_EXISTS_CODES = {10020101001, 10010101001}
513+
_ALREADY_EXISTS_CODES: set[int] = {
514+
10020101001, # 国内站 - 数据集已存在
515+
10010101001, # 国内站 - 模型已存在
516+
10010202004, # 国际站 - 名称已被使用
517+
}
518+
519+
_ALREADY_EXISTS_KEYWORDS: frozenset[str] = frozenset({
520+
"exist",
521+
"already",
522+
"can not be used",
523+
"not available",
524+
"已被注册",
525+
"已存在",
526+
"名称不可用",
527+
})
485528

486529

487530
def is_repo_exists_error(exc: BaseException) -> bool:
488-
"""Detect "repo already exists" regardless of locale or error format."""
531+
"""Detect "repo already exists" errors.
532+
533+
With the introduction of :class:`AlreadyExistsError`, this is now
534+
primarily a simple ``isinstance`` check. The keyword/code fallback
535+
is retained for backward compatibility with legacy exceptions that
536+
pre-date the structured error hierarchy.
537+
"""
538+
if isinstance(exc, AlreadyExistsError):
539+
return True
540+
# Fallback: legacy exceptions that may not be AlreadyExistsError
489541
msg = str(exc).lower()
490-
if "exist" in msg or "已被注册" in msg or "已存在" in msg:
542+
if any(kw in msg for kw in _ALREADY_EXISTS_KEYWORDS):
491543
return True
492544
body = getattr(exc, "response_body", None)
493545
if isinstance(body, dict):
@@ -510,6 +562,7 @@ def is_repo_exists_error(exc: BaseException) -> bool:
510562
# Resource / Validation
511563
"NotExistError",
512564
"InvalidParameter",
565+
"AlreadyExistsError",
513566
# Rate limiting / Server
514567
"RateLimitError",
515568
"ServerError",

src/modelscope_hub/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)