modelscope-api 0.0.3__tar.gz → 0.0.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.
Files changed (48) hide show
  1. {modelscope_api-0.0.3/src/modelscope_api.egg-info → modelscope_api-0.0.4}/PKG-INFO +3 -1
  2. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/pyproject.toml +3 -1
  3. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/exceptions.py +2 -2
  4. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/_sub_client.py +9 -0
  5. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/modelscope_client.py +12 -0
  6. modelscope_api-0.0.4/src/modelscope_api/models/studio/studio.py +430 -0
  7. {modelscope_api-0.0.3 → modelscope_api-0.0.4/src/modelscope_api.egg-info}/PKG-INFO +3 -1
  8. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api.egg-info/requires.txt +2 -0
  9. modelscope_api-0.0.3/src/modelscope_api/models/studio/studio.py +0 -236
  10. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/LICENSE +0 -0
  11. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/README.md +0 -0
  12. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/setup.cfg +0 -0
  13. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/__init__.py +0 -0
  14. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/config.py +0 -0
  15. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/__init__.py +0 -0
  16. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/base.py +0 -0
  17. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/collection/__init__.py +0 -0
  18. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/collection/collection.py +0 -0
  19. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/collection/collection_item.py +0 -0
  20. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/magicube/__init__.py +0 -0
  21. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/magicube/magicube_blance.py +0 -0
  22. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/studio/__init__.py +0 -0
  23. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/studio/base_image.py +0 -0
  24. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/studio/environment_variable.py +0 -0
  25. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/studio/hardware.py +0 -0
  26. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/studio/logs.py +0 -0
  27. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/studio/sdk.py +0 -0
  28. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/studio/studio.py +0 -0
  29. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/user/__init__.py +0 -0
  30. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/data_models/user/user.py +0 -0
  31. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/__init__.py +0 -0
  32. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/collection/__init__.py +0 -0
  33. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/collection/collection.py +0 -0
  34. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/collection/collection_client.py +0 -0
  35. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/collection/collection_item.py +0 -0
  36. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/collection/collection_item_client.py +0 -0
  37. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/magicube/__init__.py +0 -0
  38. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/magicube/magicube_client.py +0 -0
  39. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/studio/__init__.py +0 -0
  40. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/studio/environment_variable_client.py +0 -0
  41. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/studio/studio_client.py +0 -0
  42. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/user/__init__.py +0 -0
  43. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/models/user/user_client.py +0 -0
  44. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/utils/regex.py +0 -0
  45. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api/utils/typing.py +0 -0
  46. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api.egg-info/SOURCES.txt +0 -0
  47. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api.egg-info/dependency_links.txt +0 -0
  48. {modelscope_api-0.0.3 → modelscope_api-0.0.4}/src/modelscope_api.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelscope-api
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: A Python async SDK that wraps the ModelScope API into a clean, type-safe interface.
5
5
  Author-email: Jerry <wujr24@m.fudan.edu.cn>
6
6
  License: MIT
@@ -18,8 +18,10 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
+ Requires-Dist: aiofiles
21
22
  Requires-Dist: fake-useragent
22
23
  Requires-Dist: httpx
24
+ Requires-Dist: modelscope-hub
23
25
  Requires-Dist: python-dotenv
24
26
  Requires-Dist: typing-extensions
25
27
  Requires-Dist: yarl
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "modelscope-api" # 包名,保持与项目仓库名一致
7
- version = "0.0.3" # 当前版本
7
+ version = "0.0.4" # 当前版本
8
8
  description = "A Python async SDK that wraps the ModelScope API into a clean, type-safe interface." # 简短描述
9
9
  readme = "README.md" # 自述文件,PyPI 会将其渲染为项目首页
10
10
  authors = [{name = "Jerry", email = "wujr24@m.fudan.edu.cn"}] # 作者信息
@@ -31,8 +31,10 @@ classifiers = [ # 帮助用户在 PyPI 上找到你的项目
31
31
  ]
32
32
  requires-python = ">=3.11"
33
33
  dependencies = [ # 项目的运行时依赖,从 requirements.txt 移植
34
+ "aiofiles",
34
35
  "fake-useragent",
35
36
  "httpx",
37
+ "modelscope-hub",
36
38
  "python-dotenv",
37
39
  "typing-extensions",
38
40
  "yarl",
@@ -16,10 +16,10 @@ class BusinessException(Exception):
16
16
  """
17
17
 
18
18
  # 默认消息
19
- DEFAULT_MESSAGE : ClassVar[str] = "Business Error"
19
+ DEFAULT_MESSAGE: ClassVar[str] = "Business Error"
20
20
 
21
21
  # 默认错误代码
22
- DEFAULT_CODE : ClassVar[str] = "ERROR"
22
+ DEFAULT_CODE: ClassVar[str] = "ERROR"
23
23
 
24
24
  def __init__(
25
25
  self,
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  from typing import List, Optional, Union, TYPE_CHECKING
8
8
 
9
+ from modelscope_hub import HubApi
9
10
  from yarl import URL
10
11
 
11
12
  if TYPE_CHECKING:
@@ -35,6 +36,14 @@ class SubClient:
35
36
  self.openapi_prefix: Optional[str] = openapi_prefix
36
37
 
37
38
 
39
+ @property
40
+ def hub_api(self) -> HubApi:
41
+ """
42
+ 返回一个 HubApi 对象,用于实现 OpenAPI 中未实现的操作。
43
+ """
44
+ return self.super_client.hub_api
45
+
46
+
38
47
  # ==== API ====
39
48
 
40
49
  @property
@@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Union
7
7
 
8
8
  from fake_useragent import UserAgent
9
9
  import httpx
10
+ from modelscope_hub import HubApi
10
11
  from typing_extensions import Self
11
12
  from yarl import URL
12
13
 
@@ -115,6 +116,17 @@ class ModelScopeClient:
115
116
  await self._http_client.aclose()
116
117
 
117
118
 
119
+ @property
120
+ def hub_api(self) -> HubApi:
121
+ """
122
+ 将 api_key 注入 HubApi 并返回 HubApi 对象。
123
+ """
124
+ return HubApi(
125
+ endpoint=str(self.openapi_base_url.origin()),
126
+ token=self.api_key
127
+ )
128
+
129
+
118
130
  @property
119
131
  def api_url(self) -> URL:
120
132
  """
@@ -0,0 +1,430 @@
1
+ """
2
+ 对单个创空间的操作。
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ from pathlib import Path
9
+ from typing import BinaryIO, List, Optional, TYPE_CHECKING
10
+
11
+ import aiofiles
12
+ import httpx
13
+ from yarl import URL
14
+
15
+ from ...config import STUDIO_API_DOMAIN
16
+ from ...utils.regex import STUDIO_ID_PATTERN
17
+ from ...data_models.studio import (
18
+ StudioInfo, StudioRuntimeInfo, StudioVisibility,
19
+ EnvironmentVariableType,
20
+ SDKType,
21
+ LogsInfo, LogsType
22
+ )
23
+ from .._sub_client import SubClient
24
+ from .environment_variable_client import EnvironmentVariableClient
25
+
26
+ if TYPE_CHECKING:
27
+ from .studio_client import StudioClient
28
+
29
+
30
+
31
+ class Studio(SubClient):
32
+ """
33
+ 单个创空间。
34
+ """
35
+
36
+ def __init__(self, studio_client: StudioClient, id: str):
37
+ self._id = id
38
+ assert STUDIO_ID_PATTERN.fullmatch(self.id), f"Invalid studio ID: {self.id}"
39
+ super().__init__(
40
+ super_client=studio_client,
41
+ openapi_prefix=self.id
42
+ )
43
+
44
+ # 明文变量
45
+ self.variables = EnvironmentVariableClient(
46
+ studio=self,
47
+ type=EnvironmentVariableType.VARIABLE
48
+ )
49
+
50
+ # 密文变量
51
+ self.secrets = EnvironmentVariableClient(
52
+ studio=self,
53
+ type=EnvironmentVariableType.SECRET
54
+ )
55
+
56
+
57
+ def __str__(self) -> str:
58
+ return f"{type(self).__name__}<{self.id}>"
59
+
60
+
61
+ # ==== 只读属性 ====
62
+
63
+ @property
64
+ def id(self) -> str:
65
+ """
66
+ Studio ID (owner/repo_name)
67
+ """
68
+ return self._id
69
+
70
+
71
+ @property
72
+ def owner(self) -> str:
73
+ """
74
+ 拥有者(个人用户名或组织名)
75
+ """
76
+ return self._id.split("/")[0]
77
+
78
+
79
+ @property
80
+ def repo_name(self) -> str:
81
+ """
82
+ 仓库名。
83
+ """
84
+ return self._id.split("/")[1]
85
+
86
+
87
+ # ==== 创空间操作 ====
88
+
89
+ async def get_info(self, **kwargs) -> StudioInfo:
90
+ """
91
+ 获取当前 Studio 的详细信息。
92
+
93
+ 公开(public)和公开体验(protected)类型的创空间无需认证即可访问;私有(private)创空间需要认证。
94
+ """
95
+ kwargs["method"] = "GET"
96
+ kwargs["subpath"] = None
97
+ data = await self.request_openapi_data(**kwargs)
98
+ return StudioInfo.from_json(data)
99
+
100
+
101
+ async def update_settings(
102
+ self,
103
+ *,
104
+ display_name: Optional[str] = None,
105
+ license: Optional[str] = None,
106
+ visibility: Optional[StudioVisibility] = None,
107
+ description: Optional[str] = None,
108
+ cover_image: Optional[str] = None,
109
+ sdk_type: Optional[SDKType] = None,
110
+ sdk_version: Optional[str] = None,
111
+ base_image: Optional[str] = None,
112
+ hardware: Optional[str] = None,
113
+ **kwargs
114
+ ) -> StudioInfo:
115
+ """
116
+ 更新当前 Studio 的设置,传入哪个字段修改哪个字段,不传的字段不修改。
117
+
118
+ 注意:sdk_type、sdk_version、base_image、hardware 修改后需重新部署才能生效。
119
+
120
+ Args:
121
+ display_name: 显示名称。
122
+ display_name.length <= 128
123
+ license: 许可证。
124
+ visibility: 创空间可见性。
125
+ description: 描述。
126
+ description.length <= 2000。
127
+ cover_image: 封面图 URL。
128
+ sdk_type: SDK 类型,修改后需重新部署才能生效。
129
+ sdk_version: SDK 版本,仅对 Gradio 类型生效,默认最新版,建议选用最新版本,修改后需重新部署才能生效。
130
+ base_image: 基础镜像,仅 Docker 类型不支持,建议选用最新版本,修改后需重新部署才能生效。
131
+ hardware: 硬件配置,修改后需重新部署才能生效。默认为 "platform/2v-cpu-16g-mem"。
132
+ 免费资源常见格式为 platform/...、xgpu/...、amd/...;
133
+ 付费资源格式为 paid/<InstanceType>,例如 paid/ecs.gn7i-c8g1.2xlarge。
134
+ """
135
+ kwargs["method"] = "PATCH"
136
+ kwargs["subpath"] = "settings"
137
+ kwargs.setdefault("json", {}).update({
138
+ "display_name": display_name,
139
+ "license": license,
140
+ "visibility": visibility,
141
+ "description": description,
142
+ "cover_image": cover_image,
143
+ "sdk_type": sdk_type,
144
+ "sdk_version": sdk_version,
145
+ "base_image": base_image,
146
+ "hardware": hardware,
147
+ })
148
+ data = await self.request_openapi_data(**kwargs)
149
+ return StudioInfo.from_json(data)
150
+
151
+
152
+ async def deploy(self, **kwargs) -> StudioRuntimeInfo:
153
+ """
154
+ 部署当前的 Studio(会重新拉取代码并重建),无论当前状态是停止还是运行中均可调用。
155
+ """
156
+ kwargs["method"] = "POST"
157
+ kwargs["subpath"] = "deploy"
158
+ data = await self.request_openapi_data(**kwargs)
159
+ return StudioRuntimeInfo.from_json(data)
160
+
161
+
162
+ async def stop(self, **kwargs) -> StudioRuntimeInfo:
163
+ """
164
+ 停止当前的 Studio。
165
+ """
166
+ kwargs["method"] = "POST"
167
+ kwargs["subpath"] = "stop"
168
+ data = await self.request_openapi_data(**kwargs)
169
+ return StudioRuntimeInfo.from_json(data)
170
+
171
+
172
+ async def get_logs(
173
+ self,
174
+ log_type: LogsType,
175
+ *,
176
+ page_num: int = 1,
177
+ page_size: int = 100,
178
+ keyword: Optional[str] = None,
179
+ start_timestamp: Optional[int] = None,
180
+ end_timestamp: Optional[int] = None,
181
+ **kwargs
182
+ ) -> LogsInfo:
183
+ """
184
+ 获取当前 Studio 的运行日志。
185
+
186
+ Args:
187
+ log_type: 日志类型:build(构建日志)或 run(运行日志)。
188
+ page_num: 页码,默认 1。
189
+ page_size: 每页数量,默认 100,最大 500。
190
+ keyword: 关键字过滤,可选。
191
+ start_timestamp: 开始时间戳(秒),可选,自动根据 end_timestamp 计算。
192
+ end_timestamp: 结束时间戳(秒),可选,默认当前时间。
193
+ """
194
+ kwargs["method"] = "GET"
195
+ kwargs["subpath"] = f"logs/{log_type}"
196
+ kwargs.setdefault("params", {}).update({
197
+ "page_num": page_num,
198
+ "page_size": page_size,
199
+ "keyword": keyword,
200
+ "start_timestamp": start_timestamp,
201
+ "end_timestamp": end_timestamp,
202
+ })
203
+ data = await self.request_openapi_data(**kwargs)
204
+ return LogsInfo.from_json(data)
205
+
206
+
207
+ async def delete(self) -> None:
208
+ """
209
+ 删除当前创空间。
210
+ """
211
+ await self.super_client.delete_studio(self.id)
212
+
213
+
214
+ # ==== 访问创空间 API ====
215
+
216
+ @property
217
+ def base_url(self) -> URL:
218
+ """
219
+ 创空间的 API 调用根域名,服务的基础访问入口,不带接口路径。
220
+ """
221
+ host = (
222
+ "studio-"
223
+ f"{self.owner.lower()}-"
224
+ f"{self.repo_name.lower()}."
225
+ f"{STUDIO_API_DOMAIN}"
226
+ ).replace('_', '-')
227
+ return URL.build(scheme="https", host=host)
228
+
229
+
230
+ async def request_studio_api(self, subpath: Optional[str] = None, **kwargs) -> httpx.Response:
231
+ """
232
+ 调用当前创空间的 API。
233
+ """
234
+ if subpath is None:
235
+ api_url = self.base_url
236
+ else:
237
+ api_url = self.base_url / subpath
238
+ kwargs["url"] = str(api_url)
239
+ return await self.super_client.modelscope_client.request(**kwargs)
240
+
241
+
242
+ # ==== 操作创空间文件 ====
243
+
244
+ async def upload_file(
245
+ self,
246
+ path_or_fileobj: str | Path | bytes | BinaryIO,
247
+ path_in_repo: str,
248
+ *,
249
+ commit_message: str | None = None,
250
+ commit_description: str | None = None,
251
+ revision: str | None = None,
252
+ buffer_size_mb: int = 16,
253
+ disable_tqdm: bool = False,
254
+ ) -> dict:
255
+ """Upload a single file to a repository.
256
+
257
+ LFS files upload/reuse their blob first, then commit an LFS pointer.
258
+ Normal files are committed directly with inline base64 content.
259
+ LFS mode is determined by file suffix and size threshold.
260
+
261
+ Parameters
262
+ ----------
263
+ path_or_fileobj : str, Path, bytes or BinaryIO
264
+ Local path, raw bytes, or a binary file-like object.
265
+ path_in_repo : str
266
+ Destination path inside the repository.
267
+ commit_message : str, optional
268
+ Commit message. Defaults to ``"Upload file"``.
269
+ commit_description : str, optional
270
+ Extended commit description.
271
+ revision : str, optional
272
+ Branch to commit on. Defaults to ``"master"``.
273
+ buffer_size_mb : int, optional
274
+ Buffer size in MiB for reading file data. Default 16.
275
+ disable_tqdm : bool, optional
276
+ Disable progress bar. Default False.
277
+
278
+ Returns
279
+ -------
280
+ dict
281
+ Commit info from the server.
282
+
283
+ Raises
284
+ ------
285
+ AuthenticationError
286
+ When the token is missing or invalid.
287
+ NotExistError
288
+ When the target repository does not exist.
289
+
290
+ Examples
291
+ --------
292
+ >>> studio.upload_file(
293
+ ... path_or_fileobj="./pytorch_model.bin",
294
+ ... path_in_repo="pytorch_model.bin",
295
+ ... commit_message="Add fine-tuned weights",
296
+ ... )
297
+ """
298
+ return await asyncio.to_thread(
299
+ self.hub_api.upload_file,
300
+ repo_id=self.id,
301
+ repo_type="studio",
302
+ path_or_fileobj=path_or_fileobj,
303
+ path_in_repo=path_in_repo,
304
+ commit_message=commit_message,
305
+ commit_description=commit_description,
306
+ revision=revision,
307
+ buffer_size_mb=buffer_size_mb,
308
+ disable_tqdm=disable_tqdm,
309
+ )
310
+
311
+
312
+ @staticmethod
313
+ async def _load_ignore_file(path: str | Path, encoding: str = "utf-8") -> List[str]:
314
+ """
315
+ 异步读取 ignore 文件中的 ignore_patterns。
316
+
317
+ 如果文件不存在,不会报错。
318
+ """
319
+ patterns = []
320
+ try:
321
+ async with aiofiles.open(path, mode="r", encoding=encoding) as file:
322
+ # 异步读取所有行
323
+ async for line in file:
324
+ line = line.strip()
325
+ # 跳过空行和注释行
326
+ if line and not line.startswith("#"):
327
+ patterns.append(line)
328
+ except FileNotFoundError:
329
+ pass
330
+ return patterns
331
+
332
+
333
+ async def upload_folder(
334
+ self,
335
+ folder_path: str | Path,
336
+ *,
337
+ path_in_repo: str = "",
338
+ commit_message: str | None = None,
339
+ commit_description: str | None = None,
340
+ revision: str | None = None,
341
+ allow_patterns: List[str] | None = None,
342
+ ignore_patterns: List[str] | None = None,
343
+ max_workers: int | None = None,
344
+ use_cache: bool | None = None,
345
+ disable_tqdm: bool = False,
346
+ sync_remote_repo: bool = False,
347
+ load_ignore: Optional[List[str]] = None,
348
+ ) -> dict | List[dict] | None:
349
+ """Upload an entire folder to a repository with resumable support.
350
+
351
+ Files are walked recursively from ``folder_path`` and uploaded in
352
+ parallel with adaptive batching, per-file retry, and ReAct progressive
353
+ retry fallback.
354
+
355
+ Parameters
356
+ ----------
357
+ folder_path : str or Path
358
+ Local directory whose contents will be uploaded.
359
+ path_in_repo : str, optional
360
+ Destination prefix inside the repository. Defaults to the repo root.
361
+ commit_message : str, optional
362
+ Commit message. Defaults to ``"Upload folder"``.
363
+ commit_description : str, optional
364
+ Extended commit description.
365
+ revision : str, optional
366
+ Branch to commit on. Defaults to ``"master"``.
367
+ allow_patterns : list of str, optional
368
+ If given, only files matching at least one pattern are uploaded.
369
+ ignore_patterns : list of str, optional
370
+ Files matching any pattern are skipped.
371
+ max_workers : int, optional
372
+ Concurrency for parallel uploads. Defaults to adaptive.
373
+ use_cache : bool, optional
374
+ Use resumable upload caching. When omitted, read
375
+ ``MODELSCOPE_UPLOAD_CACHE_ENABLED``. Explicit ``True`` or ``False``
376
+ overrides the environment configuration.
377
+ disable_tqdm : bool, optional
378
+ Disable progress bars. Default False.
379
+ sync_remote_repo : bool, optional
380
+ If True, delete remote files that are not present locally after
381
+ a successful upload (sync semantics). Default False.
382
+ load_ignore: list[str], optional
383
+ Each element is a relative path to ``folder_path``.
384
+ The contents of this files will be imported and added to ``ignore_matterns``.
385
+
386
+ Returns
387
+ -------
388
+ None
389
+ If all files were already committed (nothing to do).
390
+ dict
391
+ If only one batch was committed.
392
+ list of dict
393
+ If multiple batches were committed.
394
+
395
+ Examples
396
+ --------
397
+ >>> studio.upload_folder(
398
+ ... folder_path="./checkpoint-1000",
399
+ ... ignore_patterns=["*.optim", "events.out.*"],
400
+ ... max_workers=8,
401
+ ... )
402
+ """
403
+ # Fill `ignore_patterns`
404
+ if load_ignore:
405
+ folder_path = Path(folder_path)
406
+ ignore_patterns = ignore_patterns or []
407
+ tasks = []
408
+ for ignore_file_relative_path in load_ignore:
409
+ ignore_file_path = folder_path / ignore_file_relative_path
410
+ tasks.append(self._load_ignore_file(ignore_file_path))
411
+ for patterns in await asyncio.gather(*tasks):
412
+ ignore_patterns.extend(patterns)
413
+
414
+ # Request
415
+ return await asyncio.to_thread(
416
+ self.hub_api.upload_folder,
417
+ repo_id=self.id,
418
+ repo_type="studio",
419
+ folder_path=folder_path,
420
+ path_in_repo=path_in_repo,
421
+ commit_message=commit_message,
422
+ commit_description=commit_description,
423
+ revision=revision,
424
+ allow_patterns=allow_patterns,
425
+ ignore_patterns=ignore_patterns,
426
+ max_workers=max_workers,
427
+ use_cache=use_cache,
428
+ disable_tqdm=disable_tqdm,
429
+ sync_remote_repo=sync_remote_repo,
430
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelscope-api
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: A Python async SDK that wraps the ModelScope API into a clean, type-safe interface.
5
5
  Author-email: Jerry <wujr24@m.fudan.edu.cn>
6
6
  License: MIT
@@ -18,8 +18,10 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
+ Requires-Dist: aiofiles
21
22
  Requires-Dist: fake-useragent
22
23
  Requires-Dist: httpx
24
+ Requires-Dist: modelscope-hub
23
25
  Requires-Dist: python-dotenv
24
26
  Requires-Dist: typing-extensions
25
27
  Requires-Dist: yarl
@@ -1,5 +1,7 @@
1
+ aiofiles
1
2
  fake-useragent
2
3
  httpx
4
+ modelscope-hub
3
5
  python-dotenv
4
6
  typing-extensions
5
7
  yarl
@@ -1,236 +0,0 @@
1
- """
2
- 对单个创空间的操作。
3
- """
4
-
5
- from __future__ import annotations
6
-
7
- from typing import Optional, TYPE_CHECKING
8
-
9
- import httpx
10
- from yarl import URL
11
-
12
- from ...config import STUDIO_API_DOMAIN
13
- from ...utils.regex import STUDIO_ID_PATTERN
14
- from ...data_models.studio import (
15
- StudioInfo, StudioRuntimeInfo, StudioVisibility,
16
- EnvironmentVariableType,
17
- SDKType,
18
- LogsInfo, LogsType
19
- )
20
- from .._sub_client import SubClient
21
- from .environment_variable_client import EnvironmentVariableClient
22
-
23
- if TYPE_CHECKING:
24
- from .studio_client import StudioClient
25
-
26
-
27
-
28
- class Studio(SubClient):
29
- """
30
- 单个创空间。
31
- """
32
-
33
- def __init__(self, studio_client: StudioClient, id: str):
34
- self._id = id
35
- assert STUDIO_ID_PATTERN.fullmatch(self.id), f"Invalid studio ID: {self.id}"
36
- super().__init__(
37
- super_client=studio_client,
38
- openapi_prefix=self.id
39
- )
40
-
41
- # 明文变量
42
- self.variables = EnvironmentVariableClient(
43
- studio=self,
44
- type=EnvironmentVariableType.VARIABLE
45
- )
46
-
47
- # 密文变量
48
- self.secrets = EnvironmentVariableClient(
49
- studio=self,
50
- type=EnvironmentVariableType.SECRET
51
- )
52
-
53
-
54
- def __str__(self) -> str:
55
- return f"{type(self).__name__}<{self.id}>"
56
-
57
-
58
- # ==== 只读属性 ====
59
-
60
- @property
61
- def id(self) -> str:
62
- """
63
- Studio ID (owner/repo_name)
64
- """
65
- return self._id
66
-
67
-
68
- @property
69
- def owner(self) -> str:
70
- """
71
- 拥有者(个人用户名或组织名)
72
- """
73
- return self._id.split("/")[0]
74
-
75
-
76
- @property
77
- def repo_name(self) -> str:
78
- """
79
- 仓库名。
80
- """
81
- return self._id.split("/")[1]
82
-
83
-
84
- # ==== 创空间操作 ====
85
-
86
- async def get_info(self, **kwargs) -> StudioInfo:
87
- """
88
- 获取当前 Studio 的详细信息。
89
-
90
- 公开(public)和公开体验(protected)类型的创空间无需认证即可访问;私有(private)创空间需要认证。
91
- """
92
- kwargs["method"] = "GET"
93
- kwargs["subpath"] = None
94
- data = await self.request_openapi_data(**kwargs)
95
- return StudioInfo.from_json(data)
96
-
97
-
98
- async def update_settings(
99
- self,
100
- *,
101
- display_name: Optional[str] = None,
102
- license: Optional[str] = None,
103
- visibility: Optional[StudioVisibility] = None,
104
- description: Optional[str] = None,
105
- cover_image: Optional[str] = None,
106
- sdk_type: Optional[SDKType] = None,
107
- sdk_version: Optional[str] = None,
108
- base_image: Optional[str] = None,
109
- hardware: Optional[str] = None,
110
- **kwargs
111
- ) -> StudioInfo:
112
- """
113
- 更新当前 Studio 的设置,传入哪个字段修改哪个字段,不传的字段不修改。
114
-
115
- 注意:sdk_type、sdk_version、base_image、hardware 修改后需重新部署才能生效。
116
-
117
- Args:
118
- display_name: 显示名称。
119
- display_name.length <= 128
120
- license: 许可证。
121
- visibility: 创空间可见性。
122
- description: 描述。
123
- description.length <= 2000。
124
- cover_image: 封面图 URL。
125
- sdk_type: SDK 类型,修改后需重新部署才能生效。
126
- sdk_version: SDK 版本,仅对 Gradio 类型生效,默认最新版,建议选用最新版本,修改后需重新部署才能生效。
127
- base_image: 基础镜像,仅 Docker 类型不支持,建议选用最新版本,修改后需重新部署才能生效。
128
- hardware: 硬件配置,修改后需重新部署才能生效。默认为 "platform/2v-cpu-16g-mem"。
129
- 免费资源常见格式为 platform/...、xgpu/...、amd/...;
130
- 付费资源格式为 paid/<InstanceType>,例如 paid/ecs.gn7i-c8g1.2xlarge。
131
- """
132
- kwargs["method"] = "PATCH"
133
- kwargs["subpath"] = "settings"
134
- kwargs.setdefault("json", {}).update({
135
- "display_name": display_name,
136
- "license": license,
137
- "visibility": visibility,
138
- "description": description,
139
- "cover_image": cover_image,
140
- "sdk_type": sdk_type,
141
- "sdk_version": sdk_version,
142
- "base_image": base_image,
143
- "hardware": hardware,
144
- })
145
- data = await self.request_openapi_data(**kwargs)
146
- return StudioInfo.from_json(data)
147
-
148
-
149
- async def deploy(self, **kwargs) -> StudioRuntimeInfo:
150
- """
151
- 部署当前的 Studio(会重新拉取代码并重建),无论当前状态是停止还是运行中均可调用。
152
- """
153
- kwargs["method"] = "POST"
154
- kwargs["subpath"] = "deploy"
155
- data = await self.request_openapi_data(**kwargs)
156
- return StudioRuntimeInfo.from_json(data)
157
-
158
-
159
- async def stop(self, **kwargs) -> StudioRuntimeInfo:
160
- """
161
- 停止当前的 Studio。
162
- """
163
- kwargs["method"] = "POST"
164
- kwargs["subpath"] = "stop"
165
- data = await self.request_openapi_data(**kwargs)
166
- return StudioRuntimeInfo.from_json(data)
167
-
168
-
169
- async def get_logs(
170
- self,
171
- log_type: LogsType,
172
- *,
173
- page_num: int = 1,
174
- page_size: int = 100,
175
- keyword: Optional[str] = None,
176
- start_timestamp: Optional[int] = None,
177
- end_timestamp: Optional[int] = None,
178
- **kwargs
179
- ) -> LogsInfo:
180
- """
181
- 获取当前 Studio 的运行日志。
182
-
183
- Args:
184
- log_type: 日志类型:build(构建日志)或 run(运行日志)。
185
- page_num: 页码,默认 1。
186
- page_size: 每页数量,默认 100,最大 500。
187
- keyword: 关键字过滤,可选。
188
- start_timestamp: 开始时间戳(秒),可选,自动根据 end_timestamp 计算。
189
- end_timestamp: 结束时间戳(秒),可选,默认当前时间。
190
- """
191
- kwargs["method"] = "GET"
192
- kwargs["subpath"] = f"logs/{log_type}"
193
- kwargs.setdefault("params", {}).update({
194
- "page_num": page_num,
195
- "page_size": page_size,
196
- "keyword": keyword,
197
- "start_timestamp": start_timestamp,
198
- "end_timestamp": end_timestamp,
199
- })
200
- data = await self.request_openapi_data(**kwargs)
201
- return LogsInfo.from_json(data)
202
-
203
-
204
- async def delete(self) -> None:
205
- """
206
- 删除当前创空间。
207
- """
208
- await self.super_client.delete_studio(self.id)
209
-
210
-
211
- # ==== 访问创空间 API ====
212
-
213
- @property
214
- def base_url(self) -> URL:
215
- """
216
- 创空间的 API 调用根域名,服务的基础访问入口,不带接口路径。
217
- """
218
- host = (
219
- "studio-"
220
- f"{self.owner.lower()}-"
221
- f"{self.repo_name.lower()}."
222
- f"{STUDIO_API_DOMAIN}"
223
- ).replace('_', '-')
224
- return URL.build(scheme="https", host=host)
225
-
226
-
227
- async def request_studio_api(self, subpath: Optional[str] = None, **kwargs) -> httpx.Response:
228
- """
229
- 调用当前创空间的 API。
230
- """
231
- if subpath is None:
232
- api_url = self.base_url
233
- else:
234
- api_url = self.base_url / subpath
235
- kwargs["url"] = str(api_url)
236
- return await self.super_client.modelscope_client.request(**kwargs)
File without changes
File without changes
File without changes