instance-repo 1.0.9.dev6__tar.gz → 1.1.0__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 (47) hide show
  1. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/PKG-INFO +1 -1
  2. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/__init__.py +1 -1
  3. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/instances.py +113 -15
  4. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/workflows.py +0 -1
  5. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/repo.py +0 -1
  6. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/store/oss.py +104 -0
  7. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/PKG-INFO +1 -1
  8. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/pyproject.toml +1 -1
  9. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/README.md +0 -0
  10. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/_routing.py +0 -0
  11. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/_site_defaults.py +0 -0
  12. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/bootstrap.py +0 -0
  13. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/cache.py +0 -0
  14. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/cli.py +0 -0
  15. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/__init__.py +0 -0
  16. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/benchmarks.py +0 -0
  17. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/config.py +0 -0
  18. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/datasets.py +0 -0
  19. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/images.py +0 -0
  20. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/reports.py +0 -0
  21. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/scaffold.py +0 -0
  22. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/splits.py +0 -0
  23. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/versions.py +0 -0
  24. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/concurrency.py +0 -0
  25. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/content.py +0 -0
  26. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/endpoints.py +0 -0
  27. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/errors.py +0 -0
  28. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/image.py +0 -0
  29. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/layout.py +0 -0
  30. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/loader.py +0 -0
  31. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/models.py +0 -0
  32. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/paths.py +0 -0
  33. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/release.py +0 -0
  34. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/retry.py +0 -0
  35. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/store/__init__.py +0 -0
  36. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/store/acr.py +0 -0
  37. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/store/base.py +0 -0
  38. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/store/registry.py +0 -0
  39. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/tasktoml.py +0 -0
  40. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/transport.py +0 -0
  41. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/validate.py +0 -0
  42. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/SOURCES.txt +0 -0
  43. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/dependency_links.txt +0 -0
  44. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/entry_points.txt +0 -0
  45. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/requires.txt +0 -0
  46. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/top_level.txt +0 -0
  47. {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: instance-repo
3
- Version: 1.0.9.dev6
3
+ Version: 1.1.0
4
4
  Summary: InstanceRepo SDK — 评测 Instance 统一存储客户端(SDK-only;控制面由 apiserver 承载)
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -18,7 +18,7 @@ from .errors import (
18
18
  TransportError,
19
19
  )
20
20
 
21
- __version__ = "1.0.9.dev6"
21
+ __version__ = "1.1.0"
22
22
 
23
23
  __all__ = [
24
24
  "Repo", "load_instances",
@@ -655,16 +655,23 @@ class InstancesClient:
655
655
 
656
656
  def pull(self, dataset: str, version: str, instance_id: str, dest_dir,
657
657
  split: str = "default", *, verify: bool = True) -> Path:
658
- """下载实例内容。自动检测格式:harbor 下载 content.tgz,swe 下载 {id}.json。
658
+ """下载实例内容。自动检测格式:harbor 下载 content.tgz,swe 下载 {id}.json,
659
+ custom 下载 {id}.json + assets 目录。
659
660
 
660
661
  格式判断优先级:
661
- 1. verify=True 且 registry 有元数据 → 从 manifest 判断(有 tgz=harbor,否则 swe)
662
- 2. 无元数据(verify=False 或实例未注册)→ 先尝试 SWE(HEAD {id}.json),
663
- 存在则下载 json;不存在则 fallback 到 harbor(下载 content.tgz)
662
+ 1. verify=True 且 registry 有元数据 → 从 manifest 判断
663
+ (有 tgz=harbor,仅 json=swe/custom,需进一步探测)
664
+ 2. 无元数据或 manifest 无法区分 swe/custom → 下载 metadata JSON 检查
665
+ metadata_model 字段(custom → 下载 assets 目录;否则 swe)
666
+ 3. metadata JSON 不存在 → fallback 到 harbor(下载 content.tgz)
664
667
  """
665
668
  validate_instance_id(instance_id, source="pull()")
666
669
  expected = None
667
670
  fmt = None # None = 未确定,需要探测
671
+ lay = self._layout()
672
+ # 已下载的元数据字节(探测阶段下载一次,swe/custom 路径复用,避免二次网络往返)。
673
+ meta_bytes: bytes | None = None
674
+
668
675
  if verify:
669
676
  meta = self._reg.get(dataset, version, instance_id, split)
670
677
  if meta is not None and meta.manifest:
@@ -677,19 +684,35 @@ class InstancesClient:
677
684
  if has_tgz:
678
685
  fmt = "harbor"
679
686
  else:
680
- # manifest 非空但无 tgz → 查找 json entry
687
+ # manifest 有 json entry 但无 tgz → swe 或 custom(需进一步探测)。
688
+ # custom 推的 manifest 由 ingest 扫描生成,与 swe 同形态;
689
+ # 唯一区分手段是读 metadata JSON 检查 metadata_model 字段。
681
690
  for m in meta.manifest:
682
691
  if m.content_format == "json":
683
- fmt = "swe"
692
+ fmt = "swe" # 暂定 swe,后续探测可能修正为 custom
684
693
  expected = m.content_digest
685
694
  break
686
- # manifest 非空但既无 tgz 也无 json → fmt 保持 None,走探测
687
- # 无法从 manifest 确定格式 → 探测 OSS。内建 store 的探测属于下载流程,
688
- # 必须走私网限定的包内方法;自定义 ContentStore 保持旧接口兼容。
689
- lay = self._layout()
695
+
696
+ # 无法从 manifest 确定 harbor → 探测 OSS 区分 swe/custom 或 fallback harbor。
697
+ idx_key = lay.metadata_key(dataset, instance_id, version=version, split=split)
698
+ sts_prefix = lay.sts_prefix(dataset)
699
+
700
+ # ── 探测辅助:下载 metadata JSON 到临时文件并解析,区分 swe / custom ──
701
+ def _probe_meta_json() -> bytes | None:
702
+ """下载 metadata JSON 并返回字节;不存在或解析失败时返回 None。"""
703
+ tmp_dest = Path(dest_dir) / f".{instance_id}.json.probe"
704
+ tmp_dest.parent.mkdir(parents=True, exist_ok=True)
705
+ try:
706
+ self._content.get_object(
707
+ idx_key, tmp_dest, sts_prefix=sts_prefix, dataset=dataset)
708
+ data = tmp_dest.read_bytes()
709
+ return data
710
+ except Exception: # noqa: BLE001 — 下载失败视为不存在
711
+ return None
712
+ finally:
713
+ tmp_dest.unlink(missing_ok=True)
714
+
690
715
  if fmt is None:
691
- idx_key = lay.metadata_key(dataset, instance_id, version=version, split=split)
692
- sts_prefix = lay.sts_prefix(dataset)
693
716
  download_probe = getattr(self._content, "_download_probe", None)
694
717
  if download_probe is not None:
695
718
  exists = download_probe(
@@ -698,22 +721,97 @@ class InstancesClient:
698
721
  exists = self._content.object_exists(
699
722
  idx_key, sts_prefix=sts_prefix)
700
723
  if exists:
701
- fmt = "swe"
724
+ # metadata JSON 存在 → 下载并解析以区分 swe / custom
725
+ fmt = "swe" # 默认;下载后可能修正为 custom
726
+ probed = _probe_meta_json()
727
+ if probed is not None:
728
+ meta_bytes = probed
729
+ try:
730
+ meta_doc = json.loads(meta_bytes)
731
+ if (isinstance(meta_doc, dict)
732
+ and meta_doc.get("metadata_model") == "custom"):
733
+ fmt = "custom"
734
+ except (json.JSONDecodeError, UnicodeDecodeError):
735
+ pass # 无法解析 → 保持 swe
702
736
  else:
703
737
  fmt = "harbor"
704
738
 
739
+ # 若 manifest 判定为 swe 但实际可能是 custom(两者 manifest 形态相同),
740
+ # 下载 metadata JSON 做最终确认。
741
+ if fmt == "swe" and meta_bytes is None:
742
+ probed = _probe_meta_json()
743
+ if probed is not None:
744
+ meta_bytes = probed
745
+ try:
746
+ meta_doc = json.loads(meta_bytes)
747
+ if (isinstance(meta_doc, dict)
748
+ and meta_doc.get("metadata_model") == "custom"):
749
+ fmt = "custom"
750
+ except (json.JSONDecodeError, UnicodeDecodeError):
751
+ pass
752
+
753
+ if fmt == "custom":
754
+ dest = Path(dest_dir) / f"{instance_id}.json"
755
+ if meta_bytes is not None:
756
+ # 已在探测阶段下载,直接写入本地(避免二次网络往返)。
757
+ dest.parent.mkdir(parents=True, exist_ok=True)
758
+ dest.write_bytes(meta_bytes)
759
+ else:
760
+ self._content.get_object(
761
+ idx_key, dest, expected_digest=expected,
762
+ sts_prefix=sts_prefix, dataset=dataset)
763
+ # 下载资产目录({split}-assets/{id}/ 前缀下的所有文件)
764
+ self._download_custom_assets(
765
+ dataset, instance_id, split, dest_dir, lay, sts_prefix)
766
+ return dest
767
+
705
768
  if fmt == "swe":
706
769
  dest = Path(dest_dir) / f"{instance_id}.json"
707
- idx_key = lay.metadata_key(dataset, instance_id, version=version, split=split)
770
+ if meta_bytes is not None:
771
+ # 已在探测阶段下载,直接写入本地。
772
+ dest.parent.mkdir(parents=True, exist_ok=True)
773
+ dest.write_bytes(meta_bytes)
774
+ return dest
708
775
  return self._content.get_object(
709
776
  idx_key, dest, expected_digest=expected,
710
- sts_prefix=lay.sts_prefix(dataset),
777
+ sts_prefix=sts_prefix,
711
778
  dataset=dataset)
779
+
712
780
  # harbor:下载 content.tgz
713
781
  dest = Path(dest_dir) / instance_id / "content.tgz"
714
782
  return self._content.get(dataset, version, instance_id, split, dest,
715
783
  expected_digest=expected, layout=lay)
716
784
 
785
+ def _download_custom_assets(self, dataset: str, instance_id: str,
786
+ split: str, dest_dir, lay, sts_prefix: str
787
+ ) -> list[Path]:
788
+ """下载 custom 格式的资产目录({split}-assets/{id}/ 前缀下的所有文件)。
789
+
790
+ 资产文件按相对路径还原到 ``{dest_dir}/{instance_id}/`` 下。返回已下载
791
+ 的本地文件路径列表。无资产时返回空列表(不报错)。
792
+ """
793
+ asset_prefix = (f"{lay.dataset_root(dataset)}"
794
+ f"{split}-assets/{instance_id}/")
795
+ list_fn = getattr(self._content, "list_objects", None)
796
+ if list_fn is None:
797
+ return []
798
+ keys = list_fn(asset_prefix)
799
+ if not keys:
800
+ return []
801
+ downloaded: list[Path] = []
802
+ dest_root = Path(dest_dir) / instance_id
803
+ for key in keys:
804
+ rel = key[len(asset_prefix):]
805
+ if not rel or rel.endswith("/"):
806
+ continue # 跳过空键和目录占位符
807
+ local_path = dest_root / rel
808
+ local_path.parent.mkdir(parents=True, exist_ok=True)
809
+ self._content.get_object(
810
+ key, local_path,
811
+ sts_prefix=sts_prefix, dataset=dataset)
812
+ downloaded.append(local_path)
813
+ return downloaded
814
+
717
815
  def pull_many(self, instance_ids: list[str], dataset: str, version: str,
718
816
  dest_dir, *, split: str = "default",
719
817
  concurrency: int = 8, continue_on_error: bool = False,
@@ -18,7 +18,6 @@ from __future__ import annotations
18
18
 
19
19
  from urllib.parse import quote
20
20
 
21
- from ..errors import TransportError
22
21
  from ..models import PagedResult, Workflow
23
22
 
24
23
  WORKFLOW_STATUS_VALUES = ("pending", "running", "awaiting_approval",
@@ -297,7 +297,6 @@ class Repo:
297
297
  """
298
298
  from pathlib import Path as _Path
299
299
  import shutil as _shutil
300
- from . import release as _R
301
300
  from .clients.config import load_profile
302
301
  from .validate import detect_format
303
302
 
@@ -17,6 +17,7 @@ from .._routing import (
17
17
  _plan_oss_route,
18
18
  _probe_private_endpoint,
19
19
  )
20
+ from ..concurrency import ItemError, map_concurrent
20
21
  from ..content import sha256_file, verify_file_digest
21
22
  from ..errors import DigestMismatch, PrivateNetworkRequired, TransportError
22
23
  from ..retry import retry_transient
@@ -384,6 +385,109 @@ class OssContentStore(BaseContentStore):
384
385
  return self._execute_oss(
385
386
  _OperationIntent.NON_DOWNLOAD, prefix, None, list_all)
386
387
 
388
+ # ── directory-level helpers(自定义格式实例 = 目录级上传/下载)──
389
+ def put_directory(self, local_dir: Path, oss_prefix: str, *,
390
+ overwrite: bool = False,
391
+ sts_prefix: str | None = None,
392
+ dataset: str | None = None,
393
+ concurrency: int = 8) -> dict:
394
+ """Upload all files under *local_dir* to OSS under *oss_prefix*.
395
+
396
+ Each file is uploaded via :meth:`put_object` so every transfer gets
397
+ proper STS credentials and endpoint routing. Files are uploaded in
398
+ parallel using :func:`~instance_repo.concurrency.map_concurrent`.
399
+
400
+ Returns a summary dict::
401
+
402
+ {"file_count": int, "total_bytes": int, "keys": list[str]}
403
+ """
404
+ local_dir = Path(local_dir)
405
+ if not local_dir.is_dir():
406
+ raise FileNotFoundError(f"local directory not found: {local_dir}")
407
+
408
+ # Normalize prefix to always end with "/" so keys are proper children.
409
+ if oss_prefix and not oss_prefix.endswith("/"):
410
+ oss_prefix += "/"
411
+
412
+ # Collect (local_path, oss_key) pairs — sorted for determinism.
413
+ files: list[tuple[Path, str]] = sorted(
414
+ ((p, oss_prefix + str(p.relative_to(local_dir)))
415
+ for p in local_dir.rglob("*") if p.is_file()),
416
+ key=lambda t: t[1],
417
+ )
418
+
419
+ total_bytes = sum(p.stat().st_size for p, _ in files)
420
+
421
+ def _upload_one(item: tuple[Path, str]) -> str:
422
+ path, key = item
423
+ self.put_object(key, path, overwrite=overwrite,
424
+ sts_prefix=sts_prefix, dataset=dataset)
425
+ return key
426
+
427
+ try:
428
+ keys = map_concurrent(_upload_one, files, concurrency=concurrency)
429
+ except ItemError as exc:
430
+ raise exc.error from exc
431
+ return {
432
+ "file_count": len(files),
433
+ "total_bytes": total_bytes,
434
+ "keys": keys,
435
+ }
436
+
437
+ def get_directory(self, oss_prefix: str, local_dir: Path, *,
438
+ expected_digests: dict[str, str] | None = None,
439
+ sts_prefix: str | None = None,
440
+ dataset: str | None = None,
441
+ concurrency: int = 8) -> Path:
442
+ """Download all objects under *oss_prefix* to *local_dir*.
443
+
444
+ Objects are listed via :meth:`list_objects` and then downloaded in
445
+ parallel. Each file is fetched via :meth:`get_object` so every
446
+ transfer gets proper STS credentials and private-download routing.
447
+
448
+ *expected_digests* optionally maps **relative paths** (e.g.
449
+ ``"subdir/file.bin"``) to their expected content digest strings
450
+ (``"sha256:<hex>"`` or bare md5). Any mismatch raises
451
+ :class:`~instance_repo.errors.DigestMismatch`.
452
+
453
+ Returns the *local_dir* path.
454
+ """
455
+ local_dir = Path(local_dir)
456
+ local_dir.mkdir(parents=True, exist_ok=True)
457
+
458
+ # Normalize prefix for consistent key slicing.
459
+ if oss_prefix and not oss_prefix.endswith("/"):
460
+ oss_prefix += "/"
461
+
462
+ keys = self.list_objects(oss_prefix)
463
+
464
+ def _download_one(key: str) -> Path:
465
+ rel = key[len(oss_prefix):]
466
+ if not rel: # skip the prefix "directory marker" itself
467
+ return local_dir
468
+ dest = local_dir / rel
469
+ dest.parent.mkdir(parents=True, exist_ok=True)
470
+ self._execute_oss(
471
+ _OperationIntent.DOWNLOAD,
472
+ sts_prefix or oss_prefix,
473
+ dataset,
474
+ lambda bucket: bucket.get_object_to_file(key, str(dest)),
475
+ )
476
+ # Per-file digest check.
477
+ if expected_digests is not None:
478
+ expected = expected_digests.get(rel)
479
+ if expected is not None and not verify_file_digest(dest, expected):
480
+ raise DigestMismatch(
481
+ f"{key}: content digest mismatch "
482
+ f"(expected {expected}, got {sha256_file(dest)})")
483
+ return dest
484
+
485
+ try:
486
+ map_concurrent(_download_one, keys, concurrency=concurrency)
487
+ except ItemError as exc:
488
+ raise exc.error from exc
489
+ return local_dir
490
+
387
491
  # ── 实例内容便捷封装(content.tgz + {id}.json) ──
388
492
  def exists(self, instance_id: str, dataset: str, version: str,
389
493
  split: str = "default", *, layout=None) -> bool:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: instance-repo
3
- Version: 1.0.9.dev6
3
+ Version: 1.1.0
4
4
  Summary: InstanceRepo SDK — 评测 Instance 统一存储客户端(SDK-only;控制面由 apiserver 承载)
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "instance-repo"
7
- version = "1.0.9.dev6"
7
+ version = "1.1.0"
8
8
  description = "InstanceRepo SDK — 评测 Instance 统一存储客户端(SDK-only;控制面由 apiserver 承载)"
9
9
  requires-python = ">=3.10"
10
10
  readme = "README.md"