xiaoshiai-hub 1.1.2__py3-none-any.whl → 1.1.3__py3-none-any.whl

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.
xiaoshiai_hub/cli.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- 小时 AI Hub SDK 命令行工具
2
+ XiaoShi AI Hub SDK 命令行工具
3
3
  """
4
4
 
5
5
  import argparse
@@ -263,6 +263,248 @@ def cmd_whoami(args):
263
263
  return 1
264
264
 
265
265
 
266
+ def _parse_repo_id(repo_id: str):
267
+ """解析仓库 ID,返回 (organization, repo_name)"""
268
+ parts = repo_id.split("/")
269
+ if len(parts) != 2:
270
+ raise ValueError(f"无效的仓库 ID: {repo_id},格式应为: 组织/仓库名")
271
+ return parts[0], parts[1]
272
+
273
+
274
+ def _get_client(args):
275
+ """创建 HubClient 实例"""
276
+ from xiaoshiai_hub.client import HubClient
277
+ token, username, password = get_auth(args)
278
+ return HubClient(
279
+ base_url=args.base_url,
280
+ username=username,
281
+ password=password,
282
+ token=token,
283
+ )
284
+
285
+
286
+ def cmd_repo_create(args):
287
+ """处理 repo-create 命令"""
288
+ try:
289
+ org, repo_name = _parse_repo_id(args.repo_id)
290
+ client = _get_client(args)
291
+
292
+ # 解析 metadata
293
+ metadata = {}
294
+ if args.license:
295
+ metadata["license"] = args.license
296
+ if args.tasks:
297
+ metadata["tasks"] = args.tasks
298
+ if args.languages:
299
+ metadata["languages"] = args.languages
300
+ if args.tags:
301
+ metadata["tags"] = args.tags
302
+ if args.frameworks:
303
+ metadata["frameworks"] = args.frameworks
304
+
305
+ client.create_repository(
306
+ organization=org,
307
+ repo_type=args.repo_type,
308
+ repo_name=repo_name,
309
+ description=args.description,
310
+ visibility=args.visibility,
311
+ metadata=metadata if metadata else None,
312
+ base_model=args.base_model,
313
+ relationship=args.relationship,
314
+ )
315
+ print(f"仓库创建成功: {org}/{repo_name}")
316
+ return 0
317
+ except Exception as e:
318
+ print(f"错误: {e}", file=sys.stderr)
319
+ return 1
320
+
321
+
322
+ def cmd_repo_update(args):
323
+ """处理 repo-update 命令"""
324
+ try:
325
+ org, repo_name = _parse_repo_id(args.repo_id)
326
+ client = _get_client(args)
327
+
328
+ # 先获取仓库当前信息
329
+ repo = client.get_repository_info(
330
+ organization=org,
331
+ repo_type=args.repo_type,
332
+ repo_name=repo_name,
333
+ )
334
+
335
+ # 使用现有值作为默认值,只更新用户指定的字段
336
+ description = args.description if args.description is not None else repo.description
337
+ visibility = args.visibility if args.visibility is not None else repo.visibility
338
+
339
+ # 合并 metadata:用户指定的覆盖现有的
340
+ metadata = dict(repo.metadata) if repo.metadata else {}
341
+ if args.license:
342
+ metadata["license"] = args.license
343
+ if args.tasks:
344
+ metadata["tasks"] = args.tasks
345
+ if args.languages:
346
+ metadata["languages"] = args.languages
347
+ if args.tags:
348
+ metadata["tags"] = args.tags
349
+ if args.frameworks:
350
+ metadata["frameworks"] = args.frameworks
351
+
352
+ # 处理 genealogy:用户指定的覆盖现有的
353
+ base_model = args.base_model
354
+ relationship = args.relationship
355
+ if repo.genealogy:
356
+ if base_model is None and 'baseModel' in repo.genealogy:
357
+ base_model = repo.genealogy['baseModel']
358
+ if relationship is None and 'relationship' in repo.genealogy:
359
+ relationship = repo.genealogy['relationship']
360
+
361
+ client.update_repository(
362
+ organization=org,
363
+ repo_type=args.repo_type,
364
+ repo_name=repo_name,
365
+ description=description,
366
+ visibility=visibility,
367
+ metadata=metadata if metadata else None,
368
+ annotations=repo.annotations,
369
+ base_model=base_model,
370
+ relationship=relationship,
371
+ )
372
+ print(f"仓库更新成功: {org}/{repo_name}")
373
+ return 0
374
+ except Exception as e:
375
+ print(f"错误: {e}", file=sys.stderr)
376
+ return 1
377
+
378
+
379
+ def cmd_repo_delete(args):
380
+ """处理 repo-delete 命令"""
381
+ try:
382
+ org, repo_name = _parse_repo_id(args.repo_id)
383
+ client = _get_client(args)
384
+
385
+ # 确认删除
386
+ if not args.yes:
387
+ confirm = input(f"确定要删除仓库 {org}/{repo_name} 吗?此操作不可恢复![y/N]: ")
388
+ if confirm.lower() != 'y':
389
+ print("已取消删除")
390
+ return 0
391
+
392
+ client.delete_repository(
393
+ organization=org,
394
+ repo_type=args.repo_type,
395
+ repo_name=repo_name,
396
+ )
397
+ print(f"仓库删除成功: {org}/{repo_name}")
398
+ return 0
399
+ except Exception as e:
400
+ print(f"错误: {e}", file=sys.stderr)
401
+ return 1
402
+
403
+
404
+ def cmd_repo_info(args):
405
+ """处理 repo-info 命令"""
406
+ try:
407
+ org, repo_name = _parse_repo_id(args.repo_id)
408
+ client = _get_client(args)
409
+
410
+ repo = client.get_repository_info(
411
+ organization=org,
412
+ repo_type=args.repo_type,
413
+ repo_name=repo_name,
414
+ )
415
+ print(f"仓库名称: {repo.name}")
416
+ print(f"组织: {repo.organization}")
417
+ print(f"所有者: {repo.owner}")
418
+ print(f"创建者: {repo.creator}")
419
+ print(f"类型: {repo.type}")
420
+ print(f"可见性: {repo.visibility}")
421
+ if repo.description:
422
+ print(f"描述: {repo.description}")
423
+ if repo.genealogy:
424
+ print(f"模型血缘: {repo.genealogy}")
425
+ if repo.metadata:
426
+ print(f"元数据: {repo.metadata}")
427
+ if repo.annotations:
428
+ print(f"注解: {repo.annotations}")
429
+ return 0
430
+ except Exception as e:
431
+ print(f"错误: {e}", file=sys.stderr)
432
+ return 1
433
+
434
+
435
+ def cmd_branch_create(args):
436
+ """处理 branch-create 命令"""
437
+ try:
438
+ org, repo_name = _parse_repo_id(args.repo_id)
439
+ client = _get_client(args)
440
+
441
+ client.create_branch(
442
+ organization=org,
443
+ repo_type=args.repo_type,
444
+ repo_name=repo_name,
445
+ branch_name=args.branch,
446
+ from_branch=args.from_branch,
447
+ )
448
+ print(f"分支创建成功: {args.branch}")
449
+ return 0
450
+ except Exception as e:
451
+ print(f"错误: {e}", file=sys.stderr)
452
+ return 1
453
+
454
+
455
+ def cmd_branch_delete(args):
456
+ """处理 branch-delete 命令"""
457
+ try:
458
+ org, repo_name = _parse_repo_id(args.repo_id)
459
+ client = _get_client(args)
460
+
461
+ # 确认删除
462
+ if not args.yes:
463
+ confirm = input(f"确定要删除分支 {args.branch} 吗?[y/N]: ")
464
+ if confirm.lower() != 'y':
465
+ print("已取消删除")
466
+ return 0
467
+
468
+ client.delete_branch(
469
+ organization=org,
470
+ repo_type=args.repo_type,
471
+ repo_name=repo_name,
472
+ branch_name=args.branch,
473
+ )
474
+ print(f"分支删除成功: {args.branch}")
475
+ return 0
476
+ except Exception as e:
477
+ print(f"错误: {e}", file=sys.stderr)
478
+ return 1
479
+
480
+
481
+ def cmd_branch_list(args):
482
+ """处理 branch-list 命令"""
483
+ try:
484
+ org, repo_name = _parse_repo_id(args.repo_id)
485
+ client = _get_client(args)
486
+
487
+ refs = client.get_repository_refs(
488
+ organization=org,
489
+ repo_type=args.repo_type,
490
+ repo_name=repo_name,
491
+ )
492
+
493
+ if not refs:
494
+ print("没有找到分支")
495
+ return 0
496
+
497
+ print(f"仓库 {org}/{repo_name} 的分支列表:")
498
+ for ref in refs:
499
+ commit_hash = ref.hash[:8] if ref.hash else 'N/A'
500
+ default_mark = " (默认)" if ref.is_default else ""
501
+ print(f" - {ref.name} [{ref.type}] (commit: {commit_hash}){default_mark}")
502
+ return 0
503
+ except Exception as e:
504
+ print(f"错误: {e}", file=sys.stderr)
505
+ return 1
506
+
507
+
266
508
  def _add_common_args(parser):
267
509
  """添加通用参数"""
268
510
  parser.add_argument(
@@ -306,15 +548,14 @@ def create_parser():
306
548
  """创建命令行参数解析器"""
307
549
  parser = argparse.ArgumentParser(
308
550
  prog="moha",
309
- description="小时 AI Hub 命令行工具 - 上传和下载模型及数据集",
551
+ description="晓石 AI Hub 命令行工具 - 模型及数据集管理",
310
552
  )
311
553
 
312
554
  subparsers = parser.add_subparsers(dest="command", help="可用命令")
313
555
 
314
556
  # ========== 上传文件夹命令 ==========
315
557
  upload_folder_parser = subparsers.add_parser(
316
- "upload-folder",
317
- aliases=["upload"],
558
+ "upload",
318
559
  help="上传文件夹到仓库",
319
560
  )
320
561
  upload_folder_parser.add_argument("folder", help="要上传的文件夹路径")
@@ -380,7 +621,6 @@ def create_parser():
380
621
  # ========== 下载仓库命令 ==========
381
622
  download_repo_parser = subparsers.add_parser(
382
623
  "download",
383
- aliases=["download-repo"],
384
624
  help="下载整个仓库",
385
625
  )
386
626
  download_repo_parser.add_argument("repo_id", help="仓库 ID(格式: 组织/仓库名)")
@@ -490,6 +730,218 @@ def create_parser():
490
730
  )
491
731
  whoami_parser.set_defaults(func=cmd_whoami)
492
732
 
733
+ # ========== 创建仓库命令 ==========
734
+ repo_create_parser = subparsers.add_parser(
735
+ "repo-create",
736
+ help="创建仓库",
737
+ )
738
+ repo_create_parser.add_argument("repo_id", help="仓库 ID(格式: 组织/仓库名)")
739
+ repo_create_parser.add_argument(
740
+ "--repo-type", "-t",
741
+ default="models",
742
+ choices=["models", "datasets"],
743
+ help="仓库类型(默认: models)",
744
+ )
745
+ repo_create_parser.add_argument(
746
+ "--description", "-d",
747
+ help="仓库描述",
748
+ )
749
+ repo_create_parser.add_argument(
750
+ "--visibility", "-v",
751
+ default="internal",
752
+ choices=["public", "internal", "private"],
753
+ help="可见性(默认: internal)",
754
+ )
755
+ repo_create_parser.add_argument(
756
+ "--license",
757
+ action="append",
758
+ help="许可证(可多次指定)",
759
+ )
760
+ repo_create_parser.add_argument(
761
+ "--tasks",
762
+ action="append",
763
+ help="任务类型(可多次指定)",
764
+ )
765
+ repo_create_parser.add_argument(
766
+ "--languages",
767
+ action="append",
768
+ help="语言(可多次指定)",
769
+ )
770
+ repo_create_parser.add_argument(
771
+ "--tags",
772
+ action="append",
773
+ help="标签(可多次指定)",
774
+ )
775
+ repo_create_parser.add_argument(
776
+ "--frameworks",
777
+ action="append",
778
+ help="框架(可多次指定)",
779
+ )
780
+ repo_create_parser.add_argument(
781
+ "--base-model",
782
+ action="append",
783
+ help="基础模型(可多次指定,格式: 组织/模型名)",
784
+ )
785
+ repo_create_parser.add_argument(
786
+ "--relationship",
787
+ choices=["adapter", "finetune", "quantized", "merge", "repackage"],
788
+ help="与基础模型的关系",
789
+ )
790
+ _add_common_args(repo_create_parser)
791
+ repo_create_parser.set_defaults(func=cmd_repo_create)
792
+
793
+ # ========== 更新仓库命令 ==========
794
+ repo_update_parser = subparsers.add_parser(
795
+ "repo-update",
796
+ help="更新仓库",
797
+ )
798
+ repo_update_parser.add_argument("repo_id", help="仓库 ID(格式: 组织/仓库名)")
799
+ repo_update_parser.add_argument(
800
+ "--repo-type", "-t",
801
+ default="models",
802
+ choices=["models", "datasets"],
803
+ help="仓库类型(默认: models)",
804
+ )
805
+ repo_update_parser.add_argument(
806
+ "--description", "-d",
807
+ help="仓库描述",
808
+ )
809
+ repo_update_parser.add_argument(
810
+ "--visibility", "-v",
811
+ choices=["public", "internal", "private"],
812
+ help="可见性",
813
+ )
814
+ repo_update_parser.add_argument(
815
+ "--license",
816
+ action="append",
817
+ help="许可证(可多次指定)",
818
+ )
819
+ repo_update_parser.add_argument(
820
+ "--tasks",
821
+ action="append",
822
+ help="任务类型(可多次指定)",
823
+ )
824
+ repo_update_parser.add_argument(
825
+ "--languages",
826
+ action="append",
827
+ help="语言(可多次指定)",
828
+ )
829
+ repo_update_parser.add_argument(
830
+ "--tags",
831
+ action="append",
832
+ help="标签(可多次指定)",
833
+ )
834
+ repo_update_parser.add_argument(
835
+ "--frameworks",
836
+ action="append",
837
+ help="框架(可多次指定)",
838
+ )
839
+ repo_update_parser.add_argument(
840
+ "--base-model",
841
+ action="append",
842
+ help="基础模型(可多次指定,格式: 组织/模型名)",
843
+ )
844
+ repo_update_parser.add_argument(
845
+ "--relationship",
846
+ choices=["adapter", "finetune", "quantized", "merge", "repackage"],
847
+ help="与基础模型的关系",
848
+ )
849
+ _add_common_args(repo_update_parser)
850
+ repo_update_parser.set_defaults(func=cmd_repo_update)
851
+
852
+ # ========== 删除仓库命令 ==========
853
+ repo_delete_parser = subparsers.add_parser(
854
+ "repo-delete",
855
+ help="删除仓库",
856
+ )
857
+ repo_delete_parser.add_argument("repo_id", help="仓库 ID(格式: 组织/仓库名)")
858
+ repo_delete_parser.add_argument(
859
+ "--repo-type", "-t",
860
+ default="models",
861
+ choices=["models", "datasets"],
862
+ help="仓库类型(默认: models)",
863
+ )
864
+ repo_delete_parser.add_argument(
865
+ "--yes", "-y",
866
+ action="store_true",
867
+ help="跳过确认提示",
868
+ )
869
+ _add_common_args(repo_delete_parser)
870
+ repo_delete_parser.set_defaults(func=cmd_repo_delete)
871
+
872
+ # ========== 查看仓库信息命令 ==========
873
+ repo_info_parser = subparsers.add_parser(
874
+ "repo-info",
875
+ help="查看仓库信息",
876
+ )
877
+ repo_info_parser.add_argument("repo_id", help="仓库 ID(格式: 组织/仓库名)")
878
+ repo_info_parser.add_argument(
879
+ "--repo-type", "-t",
880
+ default="models",
881
+ choices=["models", "datasets"],
882
+ help="仓库类型(默认: models)",
883
+ )
884
+ _add_common_args(repo_info_parser)
885
+ repo_info_parser.set_defaults(func=cmd_repo_info)
886
+
887
+ # ========== 创建分支命令 ==========
888
+ branch_create_parser = subparsers.add_parser(
889
+ "branch-create",
890
+ help="创建分支",
891
+ )
892
+ branch_create_parser.add_argument("repo_id", help="仓库 ID(格式: 组织/仓库名)")
893
+ branch_create_parser.add_argument("branch", help="要创建的分支名称")
894
+ branch_create_parser.add_argument(
895
+ "--from", "-f",
896
+ dest="from_branch",
897
+ default="main",
898
+ help="基于哪个分支创建(默认: main)",
899
+ )
900
+ branch_create_parser.add_argument(
901
+ "--repo-type", "-t",
902
+ default="models",
903
+ choices=["models", "datasets"],
904
+ help="仓库类型(默认: models)",
905
+ )
906
+ _add_common_args(branch_create_parser)
907
+ branch_create_parser.set_defaults(func=cmd_branch_create)
908
+
909
+ # ========== 删除分支命令 ==========
910
+ branch_delete_parser = subparsers.add_parser(
911
+ "branch-delete",
912
+ help="删除分支",
913
+ )
914
+ branch_delete_parser.add_argument("repo_id", help="仓库 ID(格式: 组织/仓库名)")
915
+ branch_delete_parser.add_argument("branch", help="要删除的分支名称")
916
+ branch_delete_parser.add_argument(
917
+ "--repo-type", "-t",
918
+ default="models",
919
+ choices=["models", "datasets"],
920
+ help="仓库类型(默认: models)",
921
+ )
922
+ branch_delete_parser.add_argument(
923
+ "--yes", "-y",
924
+ action="store_true",
925
+ help="跳过确认提示",
926
+ )
927
+ _add_common_args(branch_delete_parser)
928
+ branch_delete_parser.set_defaults(func=cmd_branch_delete)
929
+
930
+ # ========== 列出分支命令 ==========
931
+ branch_list_parser = subparsers.add_parser(
932
+ "branch-list",
933
+ help="列出仓库的所有分支",
934
+ )
935
+ branch_list_parser.add_argument("repo_id", help="仓库 ID(格式: 组织/仓库名)")
936
+ branch_list_parser.add_argument(
937
+ "--repo-type", "-t",
938
+ default="models",
939
+ choices=["models", "datasets"],
940
+ help="仓库类型(默认: models)",
941
+ )
942
+ _add_common_args(branch_list_parser)
943
+ branch_list_parser.set_defaults(func=cmd_branch_list)
944
+
493
945
  return parser
494
946
 
495
947
 
xiaoshiai_hub/client.py CHANGED
@@ -110,8 +110,186 @@ class HubClient:
110
110
  url = f"{self.base_url}/moha/v1/organizations/{organization}/{repo_type}/{repo_name}/encryption/set"
111
111
  response = self._make_request("PUT", url)
112
112
  if response.status_code != 200:
113
- raise HTTPError(f"Failed to set repository encrypted flag: {response.text}")
113
+ raise HTTPError(f"Failed to set repository encrypted flag: {response.text}")
114
+
115
+ # 创建分支
116
+ def create_branch(
117
+ self,
118
+ organization: str,
119
+ repo_type: str,
120
+ repo_name: str,
121
+ branch_name: str,
122
+ from_branch: str,
123
+ ) -> None:
124
+ """
125
+ 创建新分支。如果分支已存在,则直接返回。
126
+
127
+ Args:
128
+ organization: 组织名称
129
+ repo_type: 仓库类型 ("models" 或 "datasets")
130
+ repo_name: 仓库名称
131
+ branch_name: 要创建的分支名称
132
+ from_branch: 基于哪个分支创建
133
+ """
134
+ # 先检查分支是否已存在
135
+ refs = self.get_repository_refs(organization, repo_type, repo_name)
136
+ for ref in refs:
137
+ if ref.name == branch_name:
138
+ # 分支已存在,直接返回
139
+ return
140
+
141
+ # 分支不存在,创建新分支
142
+ url = f"{self.base_url}/moha/v1/organizations/{organization}/{repo_type}/{repo_name}/refs/{branch_name}"
143
+ body = {
144
+ "base": from_branch,
145
+ }
146
+ response = self._make_request("POST", url, json=body)
147
+ if response.status_code != 200:
148
+ raise HTTPError(f"Failed to create branch: {response.text}")
149
+
150
+ # 删除分支
151
+ def delete_branch(
152
+ self,
153
+ organization: str,
154
+ repo_type: str,
155
+ repo_name: str,
156
+ branch_name: str,
157
+ ) -> None:
158
+ """
159
+ 删除分支。如果分支不存在,则直接返回。
160
+
161
+ Args:
162
+ organization: 组织名称
163
+ repo_type: 仓库类型 ("models" 或 "datasets")
164
+ repo_name: 仓库名称
165
+ branch_name: 要删除的分支名称
166
+ """
167
+ # 先检查分支是否存在
168
+ refs = self.get_repository_refs(organization, repo_type, repo_name)
169
+ branch_exists = False
170
+ for ref in refs:
171
+ if ref.name == branch_name:
172
+ branch_exists = True
173
+ break
174
+
175
+ if not branch_exists:
176
+ # 分支不存在,直接返回
177
+ return
178
+
179
+ # 分支存在,删除它
180
+ url = f"{self.base_url}/moha/v1/organizations/{organization}/{repo_type}/{repo_name}/refs/{branch_name}"
181
+ response = self._make_request("DELETE", url)
182
+ if response.status_code != 200:
183
+ raise HTTPError(f"Failed to delete branch: {response.text}")
184
+
185
+
186
+ # 更新仓库,先获取仓库信息,然后更新
187
+ def update_repository(
188
+ self,
189
+ organization: str,
190
+ repo_type: str,
191
+ repo_name: str,
192
+ description: Optional[str] = None,
193
+ visibility: str = "internal",
194
+ annotations: Optional[Dict[str, str]] = None,
195
+ metadata: Optional[Dict[str, List[str]]] = None,
196
+ base_model: Optional[List[str]] = None,
197
+ relationship: Optional[str] = None,
198
+ ) -> None:
199
+ """Update repository information."""
200
+ url = f"{self.base_url}/moha/v1/organizations/{organization}/{repo_type}/{repo_name}"
201
+ body: Dict = {
202
+ "name": repo_name,
203
+ "organization": organization,
204
+ "type": repo_type,
205
+ }
206
+ if annotations:
207
+ body["annotations"] = annotations
208
+ if description:
209
+ body["description"] = description
210
+ if visibility:
211
+ body["visibility"] = visibility
212
+ if metadata:
213
+ body["metadata"] = metadata
214
+ if base_model or relationship:
215
+ body["requestgenealogy"] = {}
216
+ if base_model:
217
+ body["requestgenealogy"]["baseModel"] = base_model
218
+ if relationship:
219
+ body["requestgenealogy"]["relationship"] = relationship
220
+ response = self._make_request("PUT", url, json=body)
221
+ if response.status_code != 200:
222
+ raise HTTPError(f"Failed to update repository: {response.text}")
114
223
 
224
+ # 删除仓库
225
+ def delete_repository(
226
+ self,
227
+ organization: str,
228
+ repo_type: str,
229
+ repo_name: str,
230
+ ) -> None:
231
+ """Delete repository."""
232
+ url = f"{self.base_url}/moha/v1/organizations/{organization}/{repo_type}/{repo_name}"
233
+ response = self._make_request("DELETE", url)
234
+ if response.status_code != 200:
235
+ raise HTTPError(f"Failed to delete repository: {response.text}")
236
+
237
+ # 创建仓库
238
+ def create_repository(
239
+ self,
240
+ organization: str,
241
+ repo_type: str,
242
+ repo_name: str,
243
+ description: Optional[str] = None,
244
+ visibility: str = "internal",
245
+ metadata: Optional[Dict[str, List[str]]] = None,
246
+ base_model: Optional[List[str]] = None,
247
+ relationship: Optional[str] = None,
248
+ ) -> None:
249
+ """
250
+ 创建新仓库。
251
+
252
+ Args:
253
+ organization: 组织名称
254
+ repo_type: 仓库类型 ("models" 或 "datasets")
255
+ repo_name: 仓库名称
256
+ description: 仓库描述
257
+ visibility: 可见性 ("public", "internal", "private")
258
+ metadata: 元数据,包含 license, tasks, languages, tags, frameworks 等
259
+ annotations: 注解
260
+ base_model: 基础模型列表 (如 ["demo/yyyy"])
261
+ relationship: 与基础模型的关系 ("adapter", "finetune", "quantized", "merge", "repackage" 等)
262
+
263
+ Returns:
264
+ 创建的仓库信息
265
+ """
266
+ url = f"{self.base_url}/moha/v1/organizations/{organization}/{repo_type}"
267
+
268
+ # 构建请求体
269
+ body: Dict = {
270
+ "name": repo_name,
271
+ "organization": organization,
272
+ "visibility": visibility,
273
+ }
274
+
275
+ if description:
276
+ body["description"] = description
277
+ if metadata:
278
+ body["metadata"] = metadata
279
+
280
+ # 构建 genealogy(模型谱系)
281
+ if base_model or relationship:
282
+ body["requestgenealogy"] = {}
283
+ if base_model:
284
+ body["requestgenealogy"]["baseModel"] = base_model
285
+ if relationship:
286
+ body["requestgenealogy"]["relationship"] = relationship
287
+
288
+ response = self._make_request("POST", url, json=body)
289
+ if response.status_code != 200:
290
+ raise HTTPError(f"Failed to create repository: {response.text}")
291
+
292
+ # 获取仓库信息
115
293
  def get_repository_info(
116
294
  self,
117
295
  organization: str,
@@ -143,15 +321,25 @@ class HubClient:
143
321
  if 'metadata' in data and isinstance(data['metadata'], dict):
144
322
  metadata = data['metadata']
145
323
 
324
+ # Parse genealogy if present
325
+ genealogy: Optional[Dict] = None
326
+ if 'genealogy' in data and isinstance(data['genealogy'], dict):
327
+ genealogy = data['genealogy']
328
+
146
329
  return Repository(
147
330
  name=data.get('name', repo_name),
148
- organization=organization,
149
- type=repo_type,
331
+ organization=data.get('organization', organization),
332
+ owner=data.get('owner', ''),
333
+ creator=data.get('creator', ''),
334
+ type=data.get('type', repo_type),
335
+ visibility=data.get('visibility', 'internal'),
336
+ genealogy=genealogy,
150
337
  description=data.get('description'),
151
338
  metadata=metadata,
152
339
  annotations=annotations,
153
340
  )
154
341
 
342
+ # 获取仓库的所有分支
155
343
  def get_repository_refs(
156
344
  self,
157
345
  organization: str,
xiaoshiai_hub/types.py CHANGED
@@ -12,7 +12,11 @@ class Repository:
12
12
  """Repository information."""
13
13
  name: str
14
14
  organization: str
15
+ owner: str
16
+ creator: str
15
17
  type: str # "models" or "datasets"
18
+ visibility: str
19
+ genealogy: Optional[Dict] = None
16
20
  description: Optional[str] = None
17
21
  metadata: Dict[str, List[str]] = field(default_factory=dict) # Repository metadata
18
22
  annotations: Dict[str, str] = field(default_factory=dict) # Repository annotations/metadata
xiaoshiai_hub/upload.py CHANGED
@@ -70,55 +70,91 @@ def _calculate_file_sha256(file_path: Path) -> str:
70
70
  return sha256_hash.hexdigest()
71
71
 
72
72
 
73
- class _TqdmUploadWrapper:
74
- """Wrapper to add tqdm progress bar to file upload."""
73
+ class _ProgressFileReader:
74
+ """
75
+ 文件读取器,跟踪实际网络上传进度。
76
+
77
+ 通过分块读取文件并在每次 read() 调用后更新进度条,
78
+ 配合 requests 的流式上传,确保进度条反映实际网络传输进度。
79
+ """
75
80
 
76
- def __init__(self, file_obj, total_size, desc=None):
77
- self.file_obj = file_obj
78
- self.total_size = total_size
81
+ def __init__(self, file_path: Path, chunk_size: int = 8192, desc: Optional[str] = None):
82
+ self.file_path = file_path
83
+ self.file_size = file_path.stat().st_size
84
+ self.chunk_size = chunk_size
85
+ self.file_obj = open(file_path, 'rb')
86
+ self.bytes_read = 0
79
87
  self.pbar = None
80
88
  if tqdm:
81
89
  self.pbar = tqdm(
82
- total=total_size,
90
+ total=self.file_size,
83
91
  unit='B',
84
92
  unit_scale=True,
85
93
  unit_divisor=1024,
86
94
  desc=desc,
87
95
  )
88
96
 
89
- def read(self, size=-1):
90
- """Read data and update progress bar."""
97
+ def read(self, size: int = -1) -> bytes:
98
+ """读取数据并更新进度条。"""
99
+ if size == -1:
100
+ size = self.chunk_size
91
101
  data = self.file_obj.read(size)
92
- if self.pbar is not None and data:
93
- self.pbar.update(len(data))
102
+ if data:
103
+ self.bytes_read += len(data)
104
+ if self.pbar is not None:
105
+ self.pbar.update(len(data))
94
106
  return data
95
107
 
96
- def __enter__(self):
108
+ def __len__(self) -> int:
109
+ """返回文件大小,供 requests 设置 Content-Length。"""
110
+ return self.file_size
111
+
112
+ def __iter__(self):
113
+ """迭代器,用于流式上传。"""
97
114
  return self
98
115
 
99
- def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore[no-untyped-def]
100
- del exc_type, exc_val, exc_tb # Unused but required by context manager protocol
116
+ def __next__(self) -> bytes:
117
+ data = self.read(self.chunk_size)
118
+ if data:
119
+ return data
120
+ raise StopIteration
121
+
122
+ def close(self):
101
123
  if self.pbar is not None:
102
124
  self.pbar.close()
103
125
  self.file_obj.close()
104
126
 
127
+ def __enter__(self):
128
+ return self
129
+
130
+ def __exit__(self, exc_type, exc_val, exc_tb):
131
+ del exc_type, exc_val, exc_tb
132
+ self.close()
133
+
105
134
 
106
135
  def _upload_file_with_progress(
107
136
  upload_url: str,
108
137
  file_path: Path,
109
138
  desc: Optional[str] = None,
139
+ chunk_size: int = 1024 * 1024, # 1MB chunks
110
140
  ) -> None:
111
- """Upload file to URL with progress bar."""
141
+ """
142
+ 上传文件到 URL 并显示真实的网络传输进度。
143
+
144
+ 使用分块流式上传,进度条反映实际发送到服务器的字节数。
145
+ """
112
146
  file_size = file_path.stat().st_size
113
147
 
114
- with open(file_path, 'rb') as f:
115
- with _TqdmUploadWrapper(f, file_size, desc=desc) as wrapped_file:
116
- upload_response = requests.put(
117
- upload_url,
118
- data=wrapped_file,
119
- headers={'Content-Type': 'application/octet-stream'}
120
- )
121
- upload_response.raise_for_status()
148
+ with _ProgressFileReader(file_path, chunk_size=chunk_size, desc=desc) as reader:
149
+ upload_response = requests.put(
150
+ upload_url,
151
+ data=reader,
152
+ headers={
153
+ 'Content-Type': 'application/octet-stream',
154
+ 'Content-Length': str(file_size),
155
+ }
156
+ )
157
+ upload_response.raise_for_status()
122
158
 
123
159
 
124
160
  def _encrypt_file_if_needed(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xiaoshiai-hub
3
- Version: 1.1.2
3
+ Version: 1.1.3
4
4
  Summary: Python SDK for XiaoShi AI Hub - Upload, download, and manage AI models and datasets with xpai-enc encryption support
5
5
  Home-page: https://github.com/poxiaoyun/moha-sdk
6
6
  Author: XiaoShi AI
@@ -244,20 +244,53 @@ client = HubClient(
244
244
  password="your-password",
245
245
  )
246
246
 
247
+ # 创建仓库
248
+ repo = client.create_repository(
249
+ organization="demo",
250
+ repo_type="models",
251
+ repo_name="my-model",
252
+ description="我的模型",
253
+ visibility="internal",
254
+ metadata={
255
+ "license": ["apache-2.0"],
256
+ "frameworks": ["transformers"],
257
+ },
258
+ )
259
+ print(f"仓库已创建: {repo.name}")
260
+
247
261
  # 获取仓库信息
248
262
  repo_info = client.get_repository_info("demo", "models", "my-model")
249
263
  print(f"仓库名称: {repo_info.name}")
250
264
  print(f"组织: {repo_info.organization}")
265
+ print(f"所有者: {repo_info.owner}")
266
+ print(f"可见性: {repo_info.visibility}")
267
+
268
+ # 更新仓库
269
+ client.update_repository(
270
+ organization="demo",
271
+ repo_type="models",
272
+ repo_name="my-model",
273
+ description="更新后的描述",
274
+ )
251
275
 
252
276
  # 列出分支
253
- branches = client.list_branches("demo", "models", "my-model")
254
- for branch in branches:
255
- print(f"分支: {branch.name} (commit: {branch.commit_sha})")
277
+ refs = client.get_repository_refs("demo", "models", "my-model")
278
+ for ref in refs:
279
+ print(f"分支: {ref.name} (commit: {ref.hash[:8]})")
280
+
281
+ # 创建分支(幂等操作,已存在则直接返回)
282
+ client.create_branch("demo", "models", "my-model", "dev", "main")
283
+
284
+ # 删除分支(幂等操作,不存在则直接返回)
285
+ client.delete_branch("demo", "models", "my-model", "dev")
256
286
 
257
287
  # 浏览仓库内容
258
288
  content = client.get_repository_content("demo", "models", "my-model", "main")
259
289
  for entry in content.entries:
260
290
  print(f"{entry.type}: {entry.name}")
291
+
292
+ # 删除仓库
293
+ client.delete_repository("demo", "models", "my-model")
261
294
  ```
262
295
 
263
296
  ## 🔐 加密功能
@@ -343,7 +376,7 @@ export MOHA_ENCRYPTION_PASSWORD="your-encryption-password"
343
376
 
344
377
  ## 🖥️ 命令行工具 (CLI)
345
378
 
346
- SDK 提供了 `moha` 命令行工具,支持常见的上传下载操作。
379
+ SDK 提供了 `moha` 命令行工具,支持登录认证、仓库管理、分支管理、上传下载等操作。
347
380
 
348
381
  ### 基本用法
349
382
 
@@ -351,6 +384,75 @@ SDK 提供了 `moha` 命令行工具,支持常见的上传下载操作。
351
384
  moha --help
352
385
  ```
353
386
 
387
+ ### 登录认证
388
+
389
+ ```bash
390
+ # 登录(交互式输入用户名和密码)
391
+ moha login
392
+
393
+ # 直接指定用户名和密码
394
+ moha login --username your-username --password your-password
395
+
396
+ # 查看当前登录状态
397
+ moha whoami
398
+
399
+ # 退出登录
400
+ moha logout
401
+ ```
402
+
403
+ 登录后,Token 会保存到 `~/.moha/token.json`,后续命令无需重复输入认证信息。
404
+
405
+ ### 仓库管理
406
+
407
+ ```bash
408
+ # 创建仓库
409
+ moha repo-create org/my-model \
410
+ --description "我的模型" \
411
+ --visibility internal \
412
+ --license apache-2.0 \
413
+ --tasks text-generation \
414
+ --frameworks transformers
415
+
416
+ # 创建数据集仓库
417
+ moha repo-create org/my-dataset \
418
+ --repo-type datasets \
419
+ --description "我的数据集" \
420
+ --visibility private
421
+
422
+ # 查看仓库信息
423
+ moha repo-info org/my-model
424
+
425
+ # 更新仓库信息
426
+ moha repo-update org/my-model \
427
+ --description "更新后的描述" \
428
+ --tags production
429
+
430
+ # 删除仓库(需要确认)
431
+ moha repo-delete org/my-model
432
+
433
+ # 跳过确认直接删除
434
+ moha repo-delete org/my-model -y
435
+ ```
436
+
437
+ ### 分支管理
438
+
439
+ ```bash
440
+ # 列出仓库的所有分支
441
+ moha branch-list org/my-model
442
+
443
+ # 创建分支(基于 main 分支)
444
+ moha branch-create org/my-model dev
445
+
446
+ # 创建分支(基于指定分支)
447
+ moha branch-create org/my-model feature --from dev
448
+
449
+ # 删除分支
450
+ moha branch-delete org/my-model dev
451
+
452
+ # 跳过确认直接删除
453
+ moha branch-delete org/my-model dev -y
454
+ ```
455
+
354
456
  ### 上传文件夹
355
457
 
356
458
  ```bash
@@ -444,16 +546,42 @@ moha download-file org/my-model model.safetensors \
444
546
  --password your-password
445
547
  ```
446
548
 
549
+ ### CLI 命令列表
550
+
551
+ | 命令 | 说明 |
552
+ |------|------|
553
+ | `moha login` | 登录并保存 Token |
554
+ | `moha logout` | 退出登录并删除 Token |
555
+ | `moha whoami` | 查看当前登录状态 |
556
+ | `moha repo-create` | 创建仓库 |
557
+ | `moha repo-update` | 更新仓库 |
558
+ | `moha repo-delete` | 删除仓库 |
559
+ | `moha repo-info` | 查看仓库信息 |
560
+ | `moha branch-create` | 创建分支 |
561
+ | `moha branch-delete` | 删除分支 |
562
+ | `moha branch-list` | 列出仓库的所有分支 |
563
+ | `moha upload` | 上传文件夹到仓库 |
564
+ | `moha upload-file` | 上传单个文件到仓库 |
565
+ | `moha download` | 下载整个仓库 |
566
+ | `moha download-file` | 从仓库下载单个文件 |
567
+
447
568
  ### CLI 参数说明
448
569
 
570
+ #### 通用参数
571
+
449
572
  | 参数 | 说明 | 适用命令 |
450
573
  |------|------|----------|
451
- | `--repo-type, -t` | 仓库类型:`models` 或 `datasets`(默认:models) | 所有 |
452
- | `--revision, -r` | 分支/标签/提交(默认:main) | 所有 |
574
+ | `--repo-type, -t` | 仓库类型:`models` 或 `datasets`(默认:models) | 大部分命令 |
453
575
  | `--base-url` | API 基础 URL(默认:环境变量 MOHA_ENDPOINT) | 所有 |
454
576
  | `--token` | 认证令牌 | 所有 |
455
577
  | `--username` | 用户名 | 所有 |
456
578
  | `--password` | 密码 | 所有 |
579
+
580
+ #### 上传/下载参数
581
+
582
+ | 参数 | 说明 | 适用命令 |
583
+ |------|------|----------|
584
+ | `--revision, -r` | 分支/标签/提交(默认:main) | upload, download |
457
585
  | `--message, -m` | 提交消息 | upload, upload-file |
458
586
  | `--ignore, -i` | 忽略模式(可多次使用) | upload, download |
459
587
  | `--include` | 包含模式(可多次使用) | download |
@@ -465,6 +593,28 @@ moha download-file org/my-model model.safetensors \
465
593
  | `--local-dir, -o` | 本地保存目录 | download, download-file |
466
594
  | `--quiet, -q` | 禁用进度条 | download, download-file |
467
595
 
596
+ #### 仓库管理参数
597
+
598
+ | 参数 | 说明 | 适用命令 |
599
+ |------|------|----------|
600
+ | `--description, -d` | 仓库描述 | repo-create, repo-update |
601
+ | `--visibility, -v` | 可见性:`public`、`internal`、`private` | repo-create, repo-update |
602
+ | `--license` | 许可证(可多次使用) | repo-create, repo-update |
603
+ | `--tasks` | 任务类型(可多次使用) | repo-create, repo-update |
604
+ | `--languages` | 语言(可多次使用) | repo-create, repo-update |
605
+ | `--tags` | 标签(可多次使用) | repo-create, repo-update |
606
+ | `--frameworks` | 框架(可多次使用) | repo-create, repo-update |
607
+ | `--base-model` | 基础模型(可多次使用) | repo-create, repo-update |
608
+ | `--relationship` | 与基础模型的关系 | repo-create, repo-update |
609
+ | `--yes, -y` | 跳过确认提示 | repo-delete, branch-delete |
610
+
611
+ #### 分支管理参数
612
+
613
+ | 参数 | 说明 | 适用命令 |
614
+ |------|------|----------|
615
+ | `--from, -f` | 基于哪个分支创建(默认:main) | branch-create |
616
+ | `--yes, -y` | 跳过确认提示 | branch-delete |
617
+
468
618
  ### 使用环境变量
469
619
 
470
620
  可以通过环境变量设置认证信息,避免每次输入:
@@ -0,0 +1,15 @@
1
+ xiaoshiai_hub/__init__.py,sha256=2hnhTGad1GPTzRstfwZhAF2204Pl2VF8YHD8HQkAYGY,1469
2
+ xiaoshiai_hub/auth.py,sha256=Pv0P6f76flOefYmALyehuMu2Klyc-u_gtXlJYJR4eMY,4105
3
+ xiaoshiai_hub/cli.py,sha256=E2xWvybOz6Tr8bSk8pp-QZVHoCyQfnmCYg7442aq4-Y,29790
4
+ xiaoshiai_hub/client.py,sha256=3ASE_KuV-S0CmDvutvmMpY3GU2Hdrui2WAfwHs3iEkw,18775
5
+ xiaoshiai_hub/download.py,sha256=9Uido7cJjGVd6ERDKu_xNMPliarPdZVVb8hkLgYfFcU,13613
6
+ xiaoshiai_hub/envelope_crypto.py,sha256=zjrt5fc3ya86Y4N7_4OI5_NnmFpQ5HiHgoP31ioJRmw,8235
7
+ xiaoshiai_hub/exceptions.py,sha256=24QzgHWq_4bes07UkC3vGi2oT8SMH6Xu4FNlKt52QHo,672
8
+ xiaoshiai_hub/types.py,sha256=-h8EctZo_bRJALufjSTaxIWf2bqHz2ZDCy3SCPfucHs,2004
9
+ xiaoshiai_hub/upload.py,sha256=GKlnsxsIVuLhCWpymupya74TNgTP0s8T-gKTOR-RMgw,22371
10
+ xiaoshiai_hub-1.1.3.dist-info/licenses/LICENSE,sha256=tS28u6VpvqNisRWGeufp-XYQc6p194vOGARl3OIjidA,9110
11
+ xiaoshiai_hub-1.1.3.dist-info/METADATA,sha256=RCvKnYGmiU5upd5HXYkK0RzitGZT-Wsjvw_6CDoBa8M,21630
12
+ xiaoshiai_hub-1.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ xiaoshiai_hub-1.1.3.dist-info/entry_points.txt,sha256=fVh_IA1mbRWl7LEd4-RhENMaspSBa4Hxbsg8_HDWc6Y,48
14
+ xiaoshiai_hub-1.1.3.dist-info/top_level.txt,sha256=9AQDFb5Xn7RLQPdbk1aA0QpntbKhlhlT6Z_g-zUBtlM,14
15
+ xiaoshiai_hub-1.1.3.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- xiaoshiai_hub/__init__.py,sha256=2hnhTGad1GPTzRstfwZhAF2204Pl2VF8YHD8HQkAYGY,1469
2
- xiaoshiai_hub/auth.py,sha256=Pv0P6f76flOefYmALyehuMu2Klyc-u_gtXlJYJR4eMY,4105
3
- xiaoshiai_hub/cli.py,sha256=lBmIKlxmDk74WYttDCqF5fAuIKjvcIlETipWLyBUP4U,15292
4
- xiaoshiai_hub/client.py,sha256=Ikq-eJz1kA5KA0_PbzAKhmnPOpZT_AubRIefcCOZYBk,12113
5
- xiaoshiai_hub/download.py,sha256=9Uido7cJjGVd6ERDKu_xNMPliarPdZVVb8hkLgYfFcU,13613
6
- xiaoshiai_hub/envelope_crypto.py,sha256=zjrt5fc3ya86Y4N7_4OI5_NnmFpQ5HiHgoP31ioJRmw,8235
7
- xiaoshiai_hub/exceptions.py,sha256=24QzgHWq_4bes07UkC3vGi2oT8SMH6Xu4FNlKt52QHo,672
8
- xiaoshiai_hub/types.py,sha256=ZC5elxqles8_ODl-fCSssOzm9q8_KjA9mGpiGgObgls,1915
9
- xiaoshiai_hub/upload.py,sha256=Hb9KX-87YP7HyskHbQCtfgn11ARzzKzPX_i-fhRtZ0E,21372
10
- xiaoshiai_hub-1.1.2.dist-info/licenses/LICENSE,sha256=tS28u6VpvqNisRWGeufp-XYQc6p194vOGARl3OIjidA,9110
11
- xiaoshiai_hub-1.1.2.dist-info/METADATA,sha256=QEUR27oyHBY2bm-CIseIRQZzYFzWgtoMORZExQQeAEo,17454
12
- xiaoshiai_hub-1.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- xiaoshiai_hub-1.1.2.dist-info/entry_points.txt,sha256=fVh_IA1mbRWl7LEd4-RhENMaspSBa4Hxbsg8_HDWc6Y,48
14
- xiaoshiai_hub-1.1.2.dist-info/top_level.txt,sha256=9AQDFb5Xn7RLQPdbk1aA0QpntbKhlhlT6Z_g-zUBtlM,14
15
- xiaoshiai_hub-1.1.2.dist-info/RECORD,,