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.
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/PKG-INFO +1 -1
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/__init__.py +1 -1
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/instances.py +113 -15
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/workflows.py +0 -1
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/repo.py +0 -1
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/store/oss.py +104 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/PKG-INFO +1 -1
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/pyproject.toml +1 -1
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/README.md +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/_routing.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/_site_defaults.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/bootstrap.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/cache.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/cli.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/__init__.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/benchmarks.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/config.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/datasets.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/images.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/reports.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/scaffold.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/splits.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/clients/versions.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/concurrency.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/content.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/endpoints.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/errors.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/image.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/layout.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/loader.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/models.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/paths.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/release.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/retry.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/store/__init__.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/store/acr.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/store/base.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/store/registry.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/tasktoml.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/transport.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo/validate.py +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/SOURCES.txt +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/dependency_links.txt +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/entry_points.txt +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/requires.txt +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/top_level.txt +0 -0
- {instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/setup.cfg +0 -0
|
@@ -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
|
|
662
|
-
|
|
663
|
-
|
|
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
|
|
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
|
-
|
|
687
|
-
# 无法从 manifest
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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,
|
|
@@ -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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{instance_repo-1.0.9.dev6 → instance_repo-1.1.0}/instance_repo.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|