instance-repo 1.3.3.dev2__tar.gz → 1.3.5.dev1__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.3.3.dev2 → instance_repo-1.3.5.dev1}/PKG-INFO +1 -1
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/__init__.py +5 -1
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/clients/instances.py +334 -23
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/content.py +38 -2
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/models.py +79 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/repo.py +60 -8
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo.egg-info/PKG-INFO +1 -1
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/pyproject.toml +1 -1
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/README.md +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/_routing.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/_site_defaults.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/artifacts.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/bootstrap.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/cache.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/cli.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/clients/__init__.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/clients/benchmarks.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/clients/config.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/clients/datasets.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/clients/images.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/clients/reports.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/clients/scaffold.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/clients/splits.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/clients/versions.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/clients/workflows.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/concurrency.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/endpoints.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/errors.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/image.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/layout.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/loader.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/paths.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/release.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/retry.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/store/__init__.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/store/acr.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/store/base.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/store/oss.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/store/oss_discovery.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/store/registry.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/tasktoml.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/transport.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo/validate.py +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo.egg-info/SOURCES.txt +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo.egg-info/dependency_links.txt +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo.egg-info/entry_points.txt +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo.egg-info/requires.txt +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo.egg-info/top_level.txt +0 -0
- {instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/setup.cfg +0 -0
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
"""
|
|
6
6
|
from .repo import Repo
|
|
7
7
|
from .loader import load_instances
|
|
8
|
+
from .content import compute_source_hash
|
|
8
9
|
from .models import (
|
|
9
10
|
DatasetSeries, DatasetVersion, DatasetInstance, ManifestEntry,
|
|
10
11
|
OwnerReference, Feedback, Identity, PushResult,
|
|
11
12
|
RolloutReport, RolloutJob, ScaffoldConfig,
|
|
13
|
+
DatasetObjectDirectory, DatasetObjectFile, DatasetObjectListResult,
|
|
12
14
|
canonical_json, parse_feedbacks, append_feedback,
|
|
13
15
|
)
|
|
14
16
|
from .errors import (
|
|
@@ -18,14 +20,16 @@ from .errors import (
|
|
|
18
20
|
TransportError,
|
|
19
21
|
)
|
|
20
22
|
|
|
21
|
-
__version__ = "1.3.
|
|
23
|
+
__version__ = "1.3.5.dev1"
|
|
22
24
|
|
|
23
25
|
__all__ = [
|
|
24
26
|
"Repo", "load_instances",
|
|
25
27
|
"DatasetSeries", "DatasetVersion", "DatasetInstance", "ManifestEntry",
|
|
26
28
|
"OwnerReference", "Feedback", "Identity", "PushResult",
|
|
27
29
|
"RolloutReport", "RolloutJob", "ScaffoldConfig",
|
|
30
|
+
"DatasetObjectDirectory", "DatasetObjectFile", "DatasetObjectListResult",
|
|
28
31
|
"canonical_json", "parse_feedbacks", "append_feedback",
|
|
32
|
+
"compute_source_hash",
|
|
29
33
|
"InstanceRepoError", "LayoutError", "SchemaError", "OwnerRefMissing",
|
|
30
34
|
"ImageMissing", "Immutable", "DigestMismatch", "Forbidden", "NotVisible",
|
|
31
35
|
"TargetNotAllowed", "NetworkDenied", "CredExpired",
|
|
@@ -384,6 +384,168 @@ class InstancesClient:
|
|
|
384
384
|
result[parts[0]] = ""
|
|
385
385
|
return result
|
|
386
386
|
|
|
387
|
+
# ── 实例目录/文件列举(GET /datasets/objects)──────────────────
|
|
388
|
+
|
|
389
|
+
def list_objects(self, dataset: str, instance_id: str,
|
|
390
|
+
split: str | None = None, *,
|
|
391
|
+
version: str = "",
|
|
392
|
+
path: str | None = None,
|
|
393
|
+
page_size: int = 100,
|
|
394
|
+
max_pages: int = 50,
|
|
395
|
+
environment: str | None = None,
|
|
396
|
+
metadata_model: str | None = None,
|
|
397
|
+
dataset_split_id: str | None = None
|
|
398
|
+
) -> "DatasetObjectListResult":
|
|
399
|
+
"""列举实例资产目录下的文件/子目录(自动翻页)。
|
|
400
|
+
|
|
401
|
+
调用控制面 ``GET /apis/v1/datasets/objects``,返回指定实例的
|
|
402
|
+
``{split}-assets/{instance_id}/`` 路径下的目录和文件列表。
|
|
403
|
+
自动翻页合并所有结果,直到 ``truncated=False`` 或达到 ``max_pages``。
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
dataset : str
|
|
408
|
+
Dataset 名称(``L1/L2`` 格式)。
|
|
409
|
+
instance_id : str
|
|
410
|
+
实例 ID。
|
|
411
|
+
split : str, optional
|
|
412
|
+
Split 名称;为 None 时使用 ``"default"``。
|
|
413
|
+
version : str
|
|
414
|
+
版本号,默认空串(split_first 布局下可为空)。
|
|
415
|
+
path : str, optional
|
|
416
|
+
子目录路径过滤(相对于实例资产根目录)。
|
|
417
|
+
为 None 或空串时列举实例根目录下全部内容;
|
|
418
|
+
非空时仅列举该路径下的内容(如 ``"solution"`` 或 ``"tests/sealed"``)。
|
|
419
|
+
page_size : int
|
|
420
|
+
每页大小,默认 100。
|
|
421
|
+
max_pages : int
|
|
422
|
+
最大翻页数(安全阀,防无限循环),默认 50。
|
|
423
|
+
environment : str, optional
|
|
424
|
+
``online`` / ``pre``,None 时用 profile 派生值。
|
|
425
|
+
metadata_model : str, optional
|
|
426
|
+
``split_first`` / ``version_first``,None 时用 profile 派生值。
|
|
427
|
+
dataset_split_id : str, optional
|
|
428
|
+
Split ID,可直接指定以避免 split 名称解析。
|
|
429
|
+
|
|
430
|
+
Returns
|
|
431
|
+
-------
|
|
432
|
+
DatasetObjectListResult
|
|
433
|
+
合并后的完整结果。``truncated`` 为 True 表示因 ``max_pages`` 截断。
|
|
434
|
+
"""
|
|
435
|
+
from urllib.parse import quote
|
|
436
|
+
from ..models import DatasetObjectListResult
|
|
437
|
+
if not (dataset or "").strip():
|
|
438
|
+
raise SchemaError("dataset is required for instances.list_objects")
|
|
439
|
+
if not (instance_id or "").strip():
|
|
440
|
+
raise SchemaError(
|
|
441
|
+
"instance_id is required for instances.list_objects")
|
|
442
|
+
split = (split or "default").strip()
|
|
443
|
+
# 计算实例资产根前缀:{split}-assets/{instance_id}/
|
|
444
|
+
instance_asset_prefix = f"{split}-assets/{instance_id.strip()}"
|
|
445
|
+
if path:
|
|
446
|
+
full_prefix = f"{instance_asset_prefix}/{path.strip().strip('/')}"
|
|
447
|
+
else:
|
|
448
|
+
full_prefix = instance_asset_prefix
|
|
449
|
+
# environment / metadata_model:优先显式值,其次 profile 派生
|
|
450
|
+
env = (environment or "").strip()
|
|
451
|
+
if not env:
|
|
452
|
+
env = (getattr(self._p, "dataset_environment", "") or "").strip()
|
|
453
|
+
mm = (metadata_model or "").strip()
|
|
454
|
+
if not mm:
|
|
455
|
+
mm = (getattr(self._p, "metadata_model", "") or "").strip()
|
|
456
|
+
# 自动翻页
|
|
457
|
+
all_dirs: list = []
|
|
458
|
+
all_files: list = []
|
|
459
|
+
tk: str | None = None
|
|
460
|
+
result = DatasetObjectListResult()
|
|
461
|
+
for _ in range(max_pages):
|
|
462
|
+
params: list[tuple[str, str]] = [
|
|
463
|
+
("dataset_name", dataset.strip()),
|
|
464
|
+
("split", split),
|
|
465
|
+
("prefix", full_prefix),
|
|
466
|
+
("page_size", str(int(page_size)))]
|
|
467
|
+
if (version or "").strip():
|
|
468
|
+
params.append(("version", version.strip()))
|
|
469
|
+
if (dataset_split_id or "").strip():
|
|
470
|
+
params.append(("dataset_split_id", dataset_split_id.strip()))
|
|
471
|
+
if tk:
|
|
472
|
+
params.append(("token", tk))
|
|
473
|
+
if env:
|
|
474
|
+
params.append(("environment", env))
|
|
475
|
+
if mm:
|
|
476
|
+
params.append(("metadata_model", mm))
|
|
477
|
+
qs = "&".join(f"{k}={quote(v, safe='')}" for k, v in params)
|
|
478
|
+
data = self._t.get(f"/apis/v1/datasets/objects?{qs}")
|
|
479
|
+
if not isinstance(data, dict):
|
|
480
|
+
break
|
|
481
|
+
page = DatasetObjectListResult.from_dict(data)
|
|
482
|
+
all_dirs.extend(page.directories)
|
|
483
|
+
all_files.extend(page.files)
|
|
484
|
+
result = page # 保留最后一页的 dataset/version/prefix 元信息
|
|
485
|
+
if not page.truncated or not page.next_token:
|
|
486
|
+
break
|
|
487
|
+
tk = page.next_token
|
|
488
|
+
result.directories = all_dirs
|
|
489
|
+
result.files = all_files
|
|
490
|
+
return result
|
|
491
|
+
|
|
492
|
+
# ── 变更检测辅助 ────────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
def list_content_hashes(self, dataset: str, version: str = "",
|
|
495
|
+
split: str | None = None, *,
|
|
496
|
+
environment: str | None = None,
|
|
497
|
+
metadata_model: str | None = None
|
|
498
|
+
) -> dict[str, str]:
|
|
499
|
+
"""批量查询远端实例的 ``source_hash``(存在 ``extend_meta`` 中)。
|
|
500
|
+
|
|
501
|
+
返回 ``{instance_id: source_hash}`` 字典。用于 ``skip_unchanged``
|
|
502
|
+
变更检测:与本地 ``compute_source_hash`` 或自定义哈希比对,
|
|
503
|
+
匹配则跳过 pack + upload。
|
|
504
|
+
"""
|
|
505
|
+
overrides = _registry_overrides(environment, metadata_model)
|
|
506
|
+
instances = self.list(dataset, version, split, **overrides)
|
|
507
|
+
result: dict[str, str] = {}
|
|
508
|
+
for inst in instances:
|
|
509
|
+
h = (inst.extend_meta or {}).get("source_hash", "")
|
|
510
|
+
if h:
|
|
511
|
+
result[inst.instance_id] = h
|
|
512
|
+
return result
|
|
513
|
+
|
|
514
|
+
# ── 实例删除 ────────────────────────────────────────────────────
|
|
515
|
+
|
|
516
|
+
def delete(self, dataset: str, version: str, instance_id: str, *,
|
|
517
|
+
split: str | None = None,
|
|
518
|
+
environment: str | None = None,
|
|
519
|
+
metadata_model: str | None = None) -> None:
|
|
520
|
+
"""删除单个实例的元数据记录(``DELETE /datasets/instances/detail``)。
|
|
521
|
+
|
|
522
|
+
仅删除 MongoDB 记录,不删除 OSS 对象。后续 ingest 扫描可能重建该记录。
|
|
523
|
+
"""
|
|
524
|
+
from urllib.parse import quote
|
|
525
|
+
if not (dataset or "").strip():
|
|
526
|
+
raise SchemaError("dataset is required for instances.delete")
|
|
527
|
+
if not (instance_id or "").strip():
|
|
528
|
+
raise SchemaError("instance_id is required for instances.delete")
|
|
529
|
+
params: list[tuple[str, str]] = [
|
|
530
|
+
("dataset_name", dataset.strip()),
|
|
531
|
+
("instance_id", instance_id.strip())]
|
|
532
|
+
if (version or "").strip():
|
|
533
|
+
params.append(("version", version.strip()))
|
|
534
|
+
if (split or "").strip():
|
|
535
|
+
params.append(("split", (split or "").strip()))
|
|
536
|
+
env = (environment or "").strip()
|
|
537
|
+
if not env:
|
|
538
|
+
env = (getattr(self._p, "dataset_environment", "") or "").strip()
|
|
539
|
+
if env:
|
|
540
|
+
params.append(("environment", env))
|
|
541
|
+
mm = (metadata_model or "").strip()
|
|
542
|
+
if not mm:
|
|
543
|
+
mm = (getattr(self._p, "metadata_model", "") or "").strip()
|
|
544
|
+
if mm:
|
|
545
|
+
params.append(("metadata_model", mm))
|
|
546
|
+
qs = "&".join(f"{k}={quote(v, safe='')}" for k, v in params)
|
|
547
|
+
self._t.delete(f"/apis/v1/datasets/instances/detail?{qs}")
|
|
548
|
+
|
|
387
549
|
def update(self, dataset: str, version: str, instance_id: str, *,
|
|
388
550
|
split: str | None = None, dataset_split_id: str | None = None,
|
|
389
551
|
manifest: list[dict] | None = None,
|
|
@@ -462,7 +624,10 @@ class InstancesClient:
|
|
|
462
624
|
split: str = "default", push_image: bool = True,
|
|
463
625
|
overwrite: bool = False, register: bool = True,
|
|
464
626
|
verify: bool = True,
|
|
465
|
-
image_release_dst_acr: str | None = None
|
|
627
|
+
image_release_dst_acr: str | None = None,
|
|
628
|
+
content_hash: str | None = None,
|
|
629
|
+
skip_unchanged: bool = False,
|
|
630
|
+
compress_level: int | None = None) -> PushResult:
|
|
466
631
|
"""推一个实例:自动检测格式(目录→harbor,.json→swe),validate → 上传。
|
|
467
632
|
|
|
468
633
|
harbor: validate → (可选)推镜像 → 上传 OSS(content.tgz + {id}.json)。
|
|
@@ -476,6 +641,14 @@ class InstancesClient:
|
|
|
476
641
|
verify=True(默认)在真正发生上传后回读校验:确认 OSS 对象确实可 HEAD 到。
|
|
477
642
|
|
|
478
643
|
image_release_dst_acr(可选):目标(prod)ACR host(仅 harbor 格式有效)。
|
|
644
|
+
|
|
645
|
+
content_hash(可选):调用方提供的内容标识(如 git tree id),
|
|
646
|
+
存入 ``extend_meta.source_hash``,供后续 ``skip_unchanged`` 比对。
|
|
647
|
+
|
|
648
|
+
skip_unchanged(可选):若为 True 且 content_hash 非空,先查远端
|
|
649
|
+
``extend_meta.source_hash``,匹配则跳过 pack + upload。
|
|
650
|
+
|
|
651
|
+
compress_level(可选):gzip 压缩级别(1-9),传 None 用默认值。
|
|
479
652
|
"""
|
|
480
653
|
local_path = Path(local_path)
|
|
481
654
|
if register and self._ingester is None:
|
|
@@ -550,14 +723,38 @@ class InstancesClient:
|
|
|
550
723
|
)
|
|
551
724
|
uid = instance_id
|
|
552
725
|
|
|
726
|
+
# 变更检测:skip_unchanged=True 时,若 content_hash 与远端一致,跳过 pack+upload。
|
|
727
|
+
if skip_unchanged and content_hash:
|
|
728
|
+
try:
|
|
729
|
+
remote = self._reg.get(dataset, version, instance_id, split)
|
|
730
|
+
if remote is not None:
|
|
731
|
+
remote_hash = (remote.extend_meta or {}).get("source_hash", "")
|
|
732
|
+
if remote_hash == content_hash:
|
|
733
|
+
logger.info("push %s: skip (source_hash match)", instance_id)
|
|
734
|
+
return PushResult(
|
|
735
|
+
instance_id=instance_id, uploaded=False,
|
|
736
|
+
skipped=True, image_pushed=False,
|
|
737
|
+
docker_image=docker_ref or None,
|
|
738
|
+
digests={}, ingest=None)
|
|
739
|
+
except (TransportError, Exception): # noqa: BLE001
|
|
740
|
+
pass # 查询失败不阻断,继续正常 push
|
|
741
|
+
# 将 content_hash 存入 extend_meta,供下次 skip_unchanged 比对
|
|
742
|
+
if content_hash:
|
|
743
|
+
extend_meta["source_hash"] = content_hash
|
|
744
|
+
meta.extend_meta = extend_meta
|
|
745
|
+
|
|
553
746
|
# 打包 + 构造 manifest(本地)→ register(pending) → 推 ACR → 上传 OSS。
|
|
554
747
|
# register 需要 manifest:apiserver 的 hasFile 校验要求至少一条
|
|
555
748
|
# content_type=file 项,manifest 缺失时 register 被 92005 拒(曾导致
|
|
556
749
|
# custom/harbor 主路径 push(register=True) 失败)。register 仍先于任何
|
|
557
750
|
# 远程写(元数据先行),manifest 在本地打包时即可算出 digest。
|
|
751
|
+
import time as _time
|
|
752
|
+
_t0 = _time.monotonic()
|
|
558
753
|
with tempfile.TemporaryDirectory(prefix="irepo_") as td:
|
|
559
754
|
tgz = Path(td) / "content.tgz"
|
|
560
|
-
digest = pack(local_path, tgz, exclude_image_tar=True
|
|
755
|
+
digest = pack(local_path, tgz, exclude_image_tar=True,
|
|
756
|
+
compresslevel=compress_level)
|
|
757
|
+
_pack_elapsed = _time.monotonic() - _t0
|
|
561
758
|
meta.manifest = _manifest(self._p.oss_bucket, dataset, version,
|
|
562
759
|
instance_id, split, digest, lay)
|
|
563
760
|
|
|
@@ -573,6 +770,11 @@ class InstancesClient:
|
|
|
573
770
|
meta.to_canonical_json(), overwrite=overwrite,
|
|
574
771
|
layout=lay)
|
|
575
772
|
skipped = bool(put.get("skipped"))
|
|
773
|
+
_upload_elapsed = _time.monotonic() - _t0 - _pack_elapsed
|
|
774
|
+
_tgz_size = tgz.stat().st_size if not skipped else 0
|
|
775
|
+
logger.info("push %s: pack=%.1fs (%d bytes) upload=%.1fs skipped=%s",
|
|
776
|
+
instance_id, _pack_elapsed, _tgz_size,
|
|
777
|
+
_upload_elapsed, skipped)
|
|
576
778
|
# 回读校验(防静默成功):只在真正发生上传时校验对象已落库;镜像推送后校验可 inspect。
|
|
577
779
|
if verify:
|
|
578
780
|
if not skipped and not self._content.exists(
|
|
@@ -807,7 +1009,13 @@ class InstancesClient:
|
|
|
807
1009
|
overwrite: bool = False, register: bool = False,
|
|
808
1010
|
verify: bool = True,
|
|
809
1011
|
concurrency: int = 8, continue_on_error: bool = False,
|
|
810
|
-
image_release_dst_acr: str | None = None
|
|
1012
|
+
image_release_dst_acr: str | None = None,
|
|
1013
|
+
skip_unchanged: bool = False,
|
|
1014
|
+
hash_func: "Callable | None" = None,
|
|
1015
|
+
content_hashes: dict[str, str] | None = None,
|
|
1016
|
+
progress_callback: "Callable | None" = None,
|
|
1017
|
+
executor=None,
|
|
1018
|
+
compress_level: int | None = None
|
|
811
1019
|
) -> list:
|
|
812
1020
|
"""并发推一批实例(默认 register=False:只传 OSS,元数据随后由 ingest 扫描入库)。
|
|
813
1021
|
|
|
@@ -824,38 +1032,113 @@ class InstancesClient:
|
|
|
824
1032
|
``owner_reference`` 整批共用同一来源引用(批量交付通常同源;需要逐实例不同来源时
|
|
825
1033
|
请自行循环 ``push``)。CLI 的 push / push-many / deliver 都把它设为必填——交付
|
|
826
1034
|
路径必须能追溯上游来源。
|
|
1035
|
+
|
|
1036
|
+
``skip_unchanged=True``:自动比对本地源文件哈希与远端 ``extend_meta.source_hash``,
|
|
1037
|
+
匹配则跳过 pack+upload。``hash_func`` 可自定义哈希函数(默认用
|
|
1038
|
+
``content.compute_source_hash``);``content_hashes`` 可直接提供
|
|
1039
|
+
``{instance_id: hash}`` 映射跳过本地计算。
|
|
1040
|
+
|
|
1041
|
+
``progress_callback(completed, total, result_or_error)``:每个 case 完成时调用。
|
|
1042
|
+
|
|
1043
|
+
``executor``:外部 ``ThreadPoolExecutor``,传入时复用(不关闭);
|
|
1044
|
+
为 None 时内部新建。
|
|
1045
|
+
|
|
1046
|
+
``compress_level``:gzip 压缩级别(1-9),透传给 ``pack()``。
|
|
827
1047
|
"""
|
|
828
1048
|
dirs = list(local_dirs)
|
|
1049
|
+
|
|
1050
|
+
# ── skip_unchanged:哈希比对 + 过滤 ──
|
|
1051
|
+
skipped_results: list[PushResult] = []
|
|
1052
|
+
hashes_map: dict[str, str] = dict(content_hashes) if content_hashes else {}
|
|
1053
|
+
if skip_unchanged and dirs:
|
|
1054
|
+
from ..content import compute_source_hash as _default_hash
|
|
1055
|
+
_hash_fn = hash_func or _default_hash
|
|
1056
|
+
# 1. 计算本地哈希(未提供的)
|
|
1057
|
+
for d in dirs:
|
|
1058
|
+
iid = Path(d).name
|
|
1059
|
+
if iid not in hashes_map:
|
|
1060
|
+
try:
|
|
1061
|
+
hashes_map[iid] = _hash_fn(str(d))
|
|
1062
|
+
except Exception: # noqa: BLE001
|
|
1063
|
+
pass # 哈希失败不阻断,视为"变化"
|
|
1064
|
+
# 2. 获取远端哈希
|
|
1065
|
+
try:
|
|
1066
|
+
remote_hashes = self.list_content_hashes(
|
|
1067
|
+
dataset, version, split)
|
|
1068
|
+
except Exception: # noqa: BLE001
|
|
1069
|
+
remote_hashes = {}
|
|
1070
|
+
# 3. 过滤:未变化的生成 skip 结果并从 dirs 移除
|
|
1071
|
+
unchanged_ids: set[str] = set()
|
|
1072
|
+
for d in dirs:
|
|
1073
|
+
iid = Path(d).name
|
|
1074
|
+
local_h = hashes_map.get(iid, "")
|
|
1075
|
+
remote_h = remote_hashes.get(iid, "")
|
|
1076
|
+
if local_h and local_h == remote_h:
|
|
1077
|
+
unchanged_ids.add(iid)
|
|
1078
|
+
skipped_results.append(PushResult(
|
|
1079
|
+
instance_id=iid, uploaded=False, skipped=True,
|
|
1080
|
+
image_pushed=False, docker_image=None,
|
|
1081
|
+
digests={}, ingest=None))
|
|
1082
|
+
if unchanged_ids:
|
|
1083
|
+
logger.info("push_many: %d/%d unchanged (skipped)",
|
|
1084
|
+
len(unchanged_ids), len(dirs))
|
|
1085
|
+
dirs = [d for d in dirs if Path(d).name not in unchanged_ids]
|
|
1086
|
+
|
|
829
1087
|
warm = getattr(self._content, "prewarm", None)
|
|
830
1088
|
if warm is not None:
|
|
831
|
-
# prewarm(prefix, dataset) 与 Go Prewarm(prefix, dataset) 对齐;
|
|
832
|
-
# dataset 用于 Dataset RBAC 签发(缺失时 apiserver 回 94020)。
|
|
833
|
-
# sts_prefix 是 dataset 级,两模型一致,但仍统一经 layout 取,避免再触 paths.*。
|
|
834
1089
|
sts_prefix = self._layout().sts_prefix(dataset)
|
|
835
1090
|
try:
|
|
836
1091
|
warm(sts_prefix, dataset)
|
|
837
1092
|
except TypeError:
|
|
838
|
-
# 向后兼容旧签名 prewarm(prefix)(用户自定义 store / 旧版 fake)。
|
|
839
1093
|
warm(sts_prefix)
|
|
840
|
-
# 推镜像时同样预热 ACR 凭据缓存(否则首轮各 worker 会并发重复签同一 host)
|
|
841
1094
|
if push_image and getattr(self._p, "acr_host", None):
|
|
842
|
-
# 签名恒用 apiserver 登记的 host(acr_host,VPC);token 实例级,
|
|
843
|
-
# 对公网 endpoint 同样有效(prefer_public 时连接走公网、签名仍走这里)。
|
|
844
1095
|
try:
|
|
845
1096
|
self._t.get_acr_credentials(self._p.acr_host)
|
|
846
|
-
except Exception: # noqa: BLE001
|
|
1097
|
+
except Exception: # noqa: BLE001
|
|
847
1098
|
pass
|
|
848
1099
|
|
|
849
1100
|
def _one(d):
|
|
850
|
-
|
|
1101
|
+
iid = Path(d).name
|
|
1102
|
+
ch = hashes_map.get(iid)
|
|
851
1103
|
return self.push(d, dataset, version,
|
|
852
1104
|
owner_reference=owner_reference, split=split,
|
|
853
1105
|
push_image=push_image, overwrite=overwrite,
|
|
854
1106
|
register=False, verify=verify,
|
|
855
|
-
image_release_dst_acr=image_release_dst_acr
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1107
|
+
image_release_dst_acr=image_release_dst_acr,
|
|
1108
|
+
content_hash=ch,
|
|
1109
|
+
compress_level=compress_level)
|
|
1110
|
+
|
|
1111
|
+
# ── 并发执行 + 进度回调 ──
|
|
1112
|
+
total = len(dirs)
|
|
1113
|
+
completed = [0] # mutable counter for closure
|
|
1114
|
+
lock = __import__("threading").Lock()
|
|
1115
|
+
|
|
1116
|
+
def _wrapped(d):
|
|
1117
|
+
result = _one(d)
|
|
1118
|
+
with lock:
|
|
1119
|
+
completed[0] += 1
|
|
1120
|
+
if progress_callback:
|
|
1121
|
+
try:
|
|
1122
|
+
progress_callback(completed[0], total, result)
|
|
1123
|
+
except Exception: # noqa: BLE001
|
|
1124
|
+
pass
|
|
1125
|
+
return result
|
|
1126
|
+
|
|
1127
|
+
if executor is not None:
|
|
1128
|
+
# 外部线程池:submit 并收集结果(保持输入顺序)
|
|
1129
|
+
futures = [executor.submit(_wrapped, d) for d in dirs]
|
|
1130
|
+
results = []
|
|
1131
|
+
for f in futures:
|
|
1132
|
+
r = f.result()
|
|
1133
|
+
results.append(r)
|
|
1134
|
+
if not continue_on_error:
|
|
1135
|
+
from ..concurrency import ItemError
|
|
1136
|
+
for r in results:
|
|
1137
|
+
if isinstance(r, Exception):
|
|
1138
|
+
raise ItemError(r)
|
|
1139
|
+
else:
|
|
1140
|
+
results = map_concurrent(_wrapped, dirs, concurrency=concurrency,
|
|
1141
|
+
return_exceptions=continue_on_error)
|
|
859
1142
|
# Close retired OSS buckets after the batch to release connection pools.
|
|
860
1143
|
# Guard for non-OSS or mock stores that don't have close_retired.
|
|
861
1144
|
_close = getattr(self._content, "close_retired", None)
|
|
@@ -865,11 +1148,13 @@ class InstancesClient:
|
|
|
865
1148
|
# 这里说明整批成功;True 时对成功子集 ingest(与 deliver 语义一致)。
|
|
866
1149
|
if register:
|
|
867
1150
|
self._after_upload(True, dataset, version, split=split)
|
|
868
|
-
|
|
1151
|
+
# 将 skipped 结果合并到返回列表(放在末尾,不影响 input 顺序的 pushed/failed 对应)
|
|
1152
|
+
return list(results) + skipped_results
|
|
869
1153
|
|
|
870
1154
|
def pull(self, dataset: str, version: str, instance_id: str, dest_dir,
|
|
871
1155
|
split: str = "default", *, verify: bool = True,
|
|
872
|
-
fallback_to_oss: bool = True
|
|
1156
|
+
fallback_to_oss: bool = True,
|
|
1157
|
+
path: str | None = None) -> Path:
|
|
873
1158
|
"""下载实例内容。自动检测格式:harbor 下载 content.tgz,swe 下载 {id}.json,
|
|
874
1159
|
custom 下载 {id}.json + assets 目录。
|
|
875
1160
|
|
|
@@ -881,6 +1166,11 @@ class InstancesClient:
|
|
|
881
1166
|
3. metadata JSON 不存在 → fallback 到 harbor(下载 content.tgz)
|
|
882
1167
|
4. dataset 未注册(92001)→ OSS 回退:list-only 枚举文件树 + 逐文件下载
|
|
883
1168
|
(仍走 DOWNLOAD 意图:仅私网,公网下载照旧禁止)
|
|
1169
|
+
|
|
1170
|
+
``path`` 参数用于部分下载(仅 custom 和 OSS 回退格式生效):
|
|
1171
|
+
为空时下载全部内容;非空时只下载该路径前缀下的文件。
|
|
1172
|
+
harbor(content.tgz)和 swe(单 JSON)格式不支持部分下载,
|
|
1173
|
+
``path`` 会被忽略。
|
|
884
1174
|
"""
|
|
885
1175
|
validate_instance_id(instance_id, source="pull()")
|
|
886
1176
|
expected = None
|
|
@@ -897,7 +1187,8 @@ class InstancesClient:
|
|
|
897
1187
|
and self._content is not None):
|
|
898
1188
|
# dataset 未注册 → OSS 回退下载(私网 + 系统调用方语义)
|
|
899
1189
|
return self._pull_unregistered(
|
|
900
|
-
dataset, version, instance_id, dest_dir, split
|
|
1190
|
+
dataset, version, instance_id, dest_dir, split,
|
|
1191
|
+
path=path)
|
|
901
1192
|
raise
|
|
902
1193
|
if meta is not None and meta.manifest:
|
|
903
1194
|
has_tgz = False
|
|
@@ -1000,7 +1291,7 @@ class InstancesClient:
|
|
|
1000
1291
|
asset_rels = [str(x) for x in meta_doc["assets"]]
|
|
1001
1292
|
self._download_custom_assets(
|
|
1002
1293
|
dataset, instance_id, version, split, dest_dir, lay, sts_prefix,
|
|
1003
|
-
asset_rels=asset_rels)
|
|
1294
|
+
asset_rels=asset_rels, path=path)
|
|
1004
1295
|
return dest
|
|
1005
1296
|
|
|
1006
1297
|
if fmt == "swe":
|
|
@@ -1023,7 +1314,8 @@ class InstancesClient:
|
|
|
1023
1314
|
def _download_custom_assets(self, dataset: str, instance_id: str,
|
|
1024
1315
|
version: str, split: str, dest_dir, lay,
|
|
1025
1316
|
sts_prefix: str,
|
|
1026
|
-
asset_rels: list[str] | None = None
|
|
1317
|
+
asset_rels: list[str] | None = None,
|
|
1318
|
+
path: str | None = None
|
|
1027
1319
|
) -> list[Path]:
|
|
1028
1320
|
"""下载 custom 格式的资产目录(与 content.tgz 同层、同版本规则)。
|
|
1029
1321
|
|
|
@@ -1033,17 +1325,25 @@ class InstancesClient:
|
|
|
1033
1325
|
``asset_rels`` 为元数据 ``assets`` 字段记录的资产相对路径清单:非 None 时
|
|
1034
1326
|
逐对象 GetObject 还原(普通用户的 dataset-RBAC STS 不含 oss:ListObjects,
|
|
1035
1327
|
不能靠 LIST 枚举);为 None(旧数据无该字段)时回退到 LIST 枚举。
|
|
1328
|
+
|
|
1329
|
+
``path`` 为子目录过滤(相对于资产根目录):非空时仅下载该路径前缀下的文件。
|
|
1036
1330
|
"""
|
|
1037
1331
|
asset_prefix = lay.assets_dir(dataset, instance_id,
|
|
1038
1332
|
version=version, split=split)
|
|
1039
1333
|
dest_root = Path(dest_dir) / instance_id
|
|
1040
1334
|
downloaded: list[Path] = []
|
|
1335
|
+
# path 过滤:非空时只下载 rel 以 path 开头的文件
|
|
1336
|
+
path_prefix = ""
|
|
1337
|
+
if path:
|
|
1338
|
+
path_prefix = path.strip().strip("/") + "/"
|
|
1041
1339
|
|
|
1042
1340
|
if asset_rels is not None:
|
|
1043
1341
|
# 元数据记录了资产清单:逐对象下载,免 LIST。
|
|
1044
1342
|
for rel in asset_rels:
|
|
1045
1343
|
if rel.endswith("/"):
|
|
1046
1344
|
continue # 目录占位符(push 侧不生成;兼容历史数据)
|
|
1345
|
+
if path_prefix and not rel.startswith(path_prefix):
|
|
1346
|
+
continue # path 过滤
|
|
1047
1347
|
_validate_custom_asset_rel(rel)
|
|
1048
1348
|
local_path = dest_root / rel
|
|
1049
1349
|
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -1057,7 +1357,9 @@ class InstancesClient:
|
|
|
1057
1357
|
list_fn = getattr(self._content, "list_objects", None)
|
|
1058
1358
|
if list_fn is None:
|
|
1059
1359
|
return []
|
|
1060
|
-
|
|
1360
|
+
# path 非空时,LIST 直接用子前缀缩小范围
|
|
1361
|
+
list_prefix = asset_prefix + path_prefix if path_prefix else asset_prefix
|
|
1362
|
+
keys = list_fn(list_prefix)
|
|
1061
1363
|
if not keys:
|
|
1062
1364
|
return []
|
|
1063
1365
|
for key in keys:
|
|
@@ -1074,7 +1376,8 @@ class InstancesClient:
|
|
|
1074
1376
|
return downloaded
|
|
1075
1377
|
|
|
1076
1378
|
def _pull_unregistered(self, dataset: str, version: str,
|
|
1077
|
-
instance_id: str, dest_dir, split: str
|
|
1379
|
+
instance_id: str, dest_dir, split: str,
|
|
1380
|
+
path: str | None = None) -> Path:
|
|
1078
1381
|
"""未注册 dataset 的 pull 回退:list-only 枚举文件树 + 逐文件下载。
|
|
1079
1382
|
|
|
1080
1383
|
下载仍走 :meth:`~instance_repo.store.oss.OssContentStore.get_object`
|
|
@@ -1083,6 +1386,8 @@ class InstancesClient:
|
|
|
1083
1386
|
STS 语义:未注册 dataset 无 RBAC 记录,GetObject STS 仅 system 白名单
|
|
1084
1387
|
调用方可签发(非 system 调用方在签发阶段收到明确错误),与历史行为一致。
|
|
1085
1388
|
|
|
1389
|
+
``path`` 为子目录过滤(相对于实例资产根目录):非空时仅下载该路径前缀下的文件。
|
|
1390
|
+
|
|
1086
1391
|
产物结构(harbor 同层,便于 CLI 整体搬移)::
|
|
1087
1392
|
|
|
1088
1393
|
{dest_dir}/{instance_id}/{instance_id}.json # metadata
|
|
@@ -1108,7 +1413,13 @@ class InstancesClient:
|
|
|
1108
1413
|
metadata_dest = dest_root / f"{instance_id}.json"
|
|
1109
1414
|
|
|
1110
1415
|
self._content.get_object(metadata_key, metadata_dest, dataset=dataset)
|
|
1416
|
+
# path 过滤:非空时只下载 rel 以 path 开头的文件
|
|
1417
|
+
path_prefix = ""
|
|
1418
|
+
if path:
|
|
1419
|
+
path_prefix = path.strip().strip("/") + "/"
|
|
1111
1420
|
for key, rel in files:
|
|
1421
|
+
if path_prefix and not rel.startswith(path_prefix):
|
|
1422
|
+
continue
|
|
1112
1423
|
local_path = dest_root / rel
|
|
1113
1424
|
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1114
1425
|
self._content.get_object(key, local_path, dataset=dataset)
|
|
@@ -63,17 +63,24 @@ def _normalize(ti: tarfile.TarInfo) -> tarfile.TarInfo:
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def pack(local_dir: str | Path, out_tgz: str | Path, *,
|
|
66
|
-
exclude_image_tar: bool = True
|
|
66
|
+
exclude_image_tar: bool = True,
|
|
67
|
+
compresslevel: int | None = None) -> str:
|
|
67
68
|
"""把 local_dir 打包成 out_tgz(gzip tar),返回 content_digest。
|
|
68
69
|
|
|
69
70
|
确定性:条目按相对路径排序、元数据(mtime/uid/gid/uname/gname)归零、gzip
|
|
70
71
|
header mtime=0,故同一目录内容恒得同一 digest(跨时间/机器可复现)。
|
|
72
|
+
|
|
73
|
+
``compresslevel``:gzip 压缩级别(1-9),默认 None 使用 gzip 库默认值(9)。
|
|
74
|
+
较低级别(如 1)可显著减少 CPU 时间但产出稍大的文件。
|
|
71
75
|
"""
|
|
72
76
|
src = Path(local_dir)
|
|
73
77
|
out = Path(out_tgz)
|
|
74
78
|
files = sorted(p for p in src.rglob("*") if p.is_file())
|
|
79
|
+
gz_kw: dict = {"filename": "", "mode": "wb", "mtime": 0}
|
|
80
|
+
if compresslevel is not None:
|
|
81
|
+
gz_kw["compresslevel"] = compresslevel
|
|
75
82
|
with open(out, "wb") as raw, \
|
|
76
|
-
gzip.GzipFile(
|
|
83
|
+
gzip.GzipFile(fileobj=raw, **gz_kw) as gz, \
|
|
77
84
|
tarfile.open(fileobj=gz, mode="w") as tar:
|
|
78
85
|
for p in files:
|
|
79
86
|
rel = p.relative_to(src).as_posix()
|
|
@@ -81,3 +88,32 @@ def pack(local_dir: str | Path, out_tgz: str | Path, *,
|
|
|
81
88
|
continue
|
|
82
89
|
tar.add(str(p), arcname="./" + rel, filter=_normalize)
|
|
83
90
|
return sha256_file(out)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def compute_source_hash(local_dir: str | Path, *,
|
|
94
|
+
exclude_image_tar: bool = True) -> str:
|
|
95
|
+
"""快速计算源目录的内容哈希(不打包、不压缩)。
|
|
96
|
+
|
|
97
|
+
遍历目录所有文件,按相对路径排序,对每个文件算 SHA-256,
|
|
98
|
+
最终对所有 ``(path, sha256)`` 对再算一次 SHA-256。
|
|
99
|
+
返回 ``"src-sha256:<hex>"``。
|
|
100
|
+
|
|
101
|
+
与 :func:`pack` 的 digest 不同:本函数不做 tar+gzip,只做文件读取 + 哈希,
|
|
102
|
+
CPU 开销大幅降低(省掉压缩),适合作为变更检测的快速指纹。
|
|
103
|
+
|
|
104
|
+
``exclude_image_tar`` 语义同 :func:`pack`。
|
|
105
|
+
"""
|
|
106
|
+
src = Path(local_dir)
|
|
107
|
+
files = sorted(p for p in src.rglob("*") if p.is_file())
|
|
108
|
+
h = hashlib.sha256()
|
|
109
|
+
for p in files:
|
|
110
|
+
rel = p.relative_to(src).as_posix()
|
|
111
|
+
if exclude_image_tar and rel == _EXCLUDE_REL:
|
|
112
|
+
continue
|
|
113
|
+
fh = hashlib.sha256()
|
|
114
|
+
with open(p, "rb") as f:
|
|
115
|
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
116
|
+
fh.update(chunk)
|
|
117
|
+
h.update(rel.encode("utf-8"))
|
|
118
|
+
h.update(fh.digest())
|
|
119
|
+
return "src-sha256:" + h.hexdigest()
|
|
@@ -871,3 +871,82 @@ class RoleBinding:
|
|
|
871
871
|
role_id=d.get("role_id", ""),
|
|
872
872
|
added_by=d.get("added_by", ""),
|
|
873
873
|
added_at=d.get("added_at", ""))
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
# ── Dataset Objects(目录/文件列举)──────────────────────────────────
|
|
877
|
+
|
|
878
|
+
@dataclass
|
|
879
|
+
class DatasetObjectDirectory:
|
|
880
|
+
"""``GET /datasets/objects`` 返回的目录项。"""
|
|
881
|
+
name: str
|
|
882
|
+
path: str
|
|
883
|
+
|
|
884
|
+
@classmethod
|
|
885
|
+
def from_dict(cls, d: dict) -> "DatasetObjectDirectory":
|
|
886
|
+
return cls(name=d.get("name", ""), path=d.get("path", ""))
|
|
887
|
+
|
|
888
|
+
def to_dict(self) -> dict:
|
|
889
|
+
return {"name": self.name, "path": self.path}
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
@dataclass
|
|
893
|
+
class DatasetObjectFile:
|
|
894
|
+
"""``GET /datasets/objects`` 返回的文件项。"""
|
|
895
|
+
name: str
|
|
896
|
+
path: str
|
|
897
|
+
size_bytes: int = 0
|
|
898
|
+
content_type: str = ""
|
|
899
|
+
previewable: bool = False
|
|
900
|
+
|
|
901
|
+
@classmethod
|
|
902
|
+
def from_dict(cls, d: dict) -> "DatasetObjectFile":
|
|
903
|
+
return cls(
|
|
904
|
+
name=d.get("name", ""),
|
|
905
|
+
path=d.get("path", ""),
|
|
906
|
+
size_bytes=int(d.get("size_bytes", 0)),
|
|
907
|
+
content_type=d.get("content_type", ""),
|
|
908
|
+
previewable=bool(d.get("previewable", False)))
|
|
909
|
+
|
|
910
|
+
def to_dict(self) -> dict:
|
|
911
|
+
return {"name": self.name, "path": self.path,
|
|
912
|
+
"size_bytes": self.size_bytes,
|
|
913
|
+
"content_type": self.content_type,
|
|
914
|
+
"previewable": self.previewable}
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
@dataclass
|
|
918
|
+
class DatasetObjectListResult:
|
|
919
|
+
"""``GET /datasets/objects`` 分页响应。
|
|
920
|
+
|
|
921
|
+
``directories`` 和 ``files`` 为当前页条目;``truncated`` 为 True 时
|
|
922
|
+
可用 ``next_token`` 继续翻页。
|
|
923
|
+
"""
|
|
924
|
+
dataset: str = ""
|
|
925
|
+
version: str = ""
|
|
926
|
+
prefix: str = ""
|
|
927
|
+
directories: list[DatasetObjectDirectory] = field(default_factory=list)
|
|
928
|
+
files: list[DatasetObjectFile] = field(default_factory=list)
|
|
929
|
+
next_token: str = ""
|
|
930
|
+
truncated: bool = False
|
|
931
|
+
|
|
932
|
+
@classmethod
|
|
933
|
+
def from_dict(cls, d: dict) -> "DatasetObjectListResult":
|
|
934
|
+
return cls(
|
|
935
|
+
dataset=d.get("dataset", ""),
|
|
936
|
+
version=d.get("version", ""),
|
|
937
|
+
prefix=d.get("prefix", ""),
|
|
938
|
+
directories=[DatasetObjectDirectory.from_dict(x)
|
|
939
|
+
for x in d.get("directories", [])],
|
|
940
|
+
files=[DatasetObjectFile.from_dict(x)
|
|
941
|
+
for x in d.get("files", [])],
|
|
942
|
+
next_token=d.get("next_token", ""),
|
|
943
|
+
truncated=bool(d.get("truncated", False)))
|
|
944
|
+
|
|
945
|
+
def to_dict(self) -> dict:
|
|
946
|
+
return {
|
|
947
|
+
"dataset": self.dataset, "version": self.version,
|
|
948
|
+
"prefix": self.prefix,
|
|
949
|
+
"directories": [d.to_dict() for d in self.directories],
|
|
950
|
+
"files": [f.to_dict() for f in self.files],
|
|
951
|
+
"next_token": self.next_token,
|
|
952
|
+
"truncated": self.truncated}
|
|
@@ -130,7 +130,15 @@ class Repo:
|
|
|
130
130
|
overwrite: bool = False, concurrency: int = 8,
|
|
131
131
|
publish: bool = False, ingest: bool = True,
|
|
132
132
|
continue_on_error: bool = False,
|
|
133
|
-
image_release_dst_acr: str | None = None,
|
|
133
|
+
image_release_dst_acr: str | None = None,
|
|
134
|
+
skip_unchanged: bool = False,
|
|
135
|
+
hash_func=None,
|
|
136
|
+
content_hashes: dict | None = None,
|
|
137
|
+
prune: bool = False,
|
|
138
|
+
progress_callback=None,
|
|
139
|
+
executor=None,
|
|
140
|
+
compress_level: int | None = None,
|
|
141
|
+
**ingest_kwargs) -> dict:
|
|
134
142
|
"""一次性交付一批实例:并发上传 OSS(register=False),再触发一次 dataset-ingest 入库。
|
|
135
143
|
|
|
136
144
|
相比逐个 ``push``(每实例一次 STS 签发 + 串行传输),本方法整批共用 STS、
|
|
@@ -138,7 +146,7 @@ class Repo:
|
|
|
138
146
|
|
|
139
147
|
``continue_on_error=False``(默认,全或无):任一实例失败即抛 ``ItemError``。
|
|
140
148
|
``continue_on_error=True``:跳过失败项,只要有成功上传就对成功子集触发 ingest,
|
|
141
|
-
返回 ``{"pushed":[
|
|
149
|
+
返回 ``{"pushed":[...], "skipped":[...], "failed":[...], "ingest":<job|None>}``;
|
|
142
150
|
全部失败时不触发 ingest(job=None)。``ingest=False`` 则只上传不入库。
|
|
143
151
|
|
|
144
152
|
``owner_reference``(可选):整批共用的上游来源引用,透传给每个 push。CLI 的
|
|
@@ -147,22 +155,48 @@ class Repo:
|
|
|
147
155
|
``image_release_dst_acr``(可选):透传给 push,把「实例↔镜像」上架映射写进
|
|
148
156
|
{id}.json 的 extend_meta,随本方法的 ingest 一并入库(见 push 文档)。
|
|
149
157
|
|
|
158
|
+
``skip_unchanged=True``:自动比对本地源文件哈希与远端 ``extend_meta.source_hash``,
|
|
159
|
+
匹配则跳过 pack+upload。``hash_func`` 可自定义哈希函数(默认用
|
|
160
|
+
``content.compute_source_hash``);``content_hashes`` 可直接提供
|
|
161
|
+
``{instance_id: hash}`` 映射跳过本地计算。
|
|
162
|
+
|
|
163
|
+
``prune=True``:交付完成后,删除远端存在但本次 local_dirs 中不存在的实例
|
|
164
|
+
(逐个调 ``instances.delete``)。注意:ingest 本身已有 prune 语义
|
|
165
|
+
(删除 OSS 中不存在的旧记录),此选项额外清理 OSS 中也已删除的情况。
|
|
166
|
+
|
|
167
|
+
``progress_callback(completed, total, result_or_error)``:每个 case 完成时调用。
|
|
168
|
+
|
|
169
|
+
``executor``:外部 ``ThreadPoolExecutor``,传入时复用(不关闭)。
|
|
170
|
+
|
|
171
|
+
``compress_level``:gzip 压缩级别(1-9),透传给 pack。
|
|
172
|
+
|
|
150
173
|
注意:单实例在"推 ACR→传 content.tgz→写 {id}.json"中途失败可能留下孤儿对象/镜像
|
|
151
174
|
(ingest 按 {id}.json 扫描,孤儿对此无害,但占用存储,需要时另行清理)。
|
|
152
175
|
"""
|
|
176
|
+
import logging as _logging
|
|
177
|
+
_logger = _logging.getLogger(__name__)
|
|
178
|
+
|
|
153
179
|
results = self.instances.push_many(
|
|
154
180
|
local_dirs, dataset, version, owner_reference=owner_reference,
|
|
155
181
|
split=split, push_image=push_image,
|
|
156
182
|
overwrite=overwrite, register=False, concurrency=concurrency,
|
|
157
183
|
continue_on_error=continue_on_error,
|
|
158
|
-
image_release_dst_acr=image_release_dst_acr
|
|
159
|
-
|
|
184
|
+
image_release_dst_acr=image_release_dst_acr,
|
|
185
|
+
skip_unchanged=skip_unchanged,
|
|
186
|
+
hash_func=hash_func,
|
|
187
|
+
content_hashes=content_hashes,
|
|
188
|
+
progress_callback=progress_callback,
|
|
189
|
+
executor=executor,
|
|
190
|
+
compress_level=compress_level)
|
|
191
|
+
pushed = [r for r in results
|
|
192
|
+
if not isinstance(r, ItemError)
|
|
193
|
+
and not getattr(r, "skipped", False)]
|
|
194
|
+
skipped = [r for r in results
|
|
195
|
+
if not isinstance(r, ItemError)
|
|
196
|
+
and getattr(r, "skipped", False)]
|
|
160
197
|
failed = [r for r in results if isinstance(r, ItemError)]
|
|
161
198
|
job = None
|
|
162
199
|
if ingest and pushed: # 仅在有成功上传时才 ingest
|
|
163
|
-
# 透传 split 给 ingest:用户若未通过 ingest_kwargs 显式指定 splits/split,
|
|
164
|
-
# 且 split 非默认值,则自动透传,避免 ingest 扫描所有 split 目录时
|
|
165
|
-
# 对未交付的空 split 报 92005(split path has no instance files)。
|
|
166
200
|
ingest_split_kwargs = {}
|
|
167
201
|
if "splits" not in ingest_kwargs and "split" not in ingest_kwargs:
|
|
168
202
|
if split and split != "default":
|
|
@@ -170,7 +204,25 @@ class Repo:
|
|
|
170
204
|
job = self.versions.ingest(
|
|
171
205
|
dataset, version, publish=publish,
|
|
172
206
|
**ingest_split_kwargs, **ingest_kwargs)
|
|
173
|
-
|
|
207
|
+
# prune:删除远端有但本次不在 local_dirs 中的实例
|
|
208
|
+
if prune and not failed:
|
|
209
|
+
local_ids = {Path(d).name for d in local_dirs}
|
|
210
|
+
try:
|
|
211
|
+
remote_instances = self.instances.list(dataset, version, split)
|
|
212
|
+
for inst in remote_instances:
|
|
213
|
+
if inst.instance_id not in local_ids:
|
|
214
|
+
try:
|
|
215
|
+
self.instances.delete(
|
|
216
|
+
dataset, version, inst.instance_id,
|
|
217
|
+
split=split)
|
|
218
|
+
_logger.info("prune: deleted %s", inst.instance_id)
|
|
219
|
+
except Exception: # noqa: BLE001
|
|
220
|
+
_logger.warning("prune: failed to delete %s",
|
|
221
|
+
inst.instance_id)
|
|
222
|
+
except Exception: # noqa: BLE001
|
|
223
|
+
_logger.warning("prune: failed to list remote instances")
|
|
224
|
+
return {"pushed": pushed, "skipped": skipped,
|
|
225
|
+
"failed": failed, "ingest": job}
|
|
174
226
|
|
|
175
227
|
# ── 源到目标交付:上传 → 元数据入库 → 发布 workflow ────────────────────────
|
|
176
228
|
def deliver_to_prod(self, dataset: str, version: str, local_dirs, *,
|
|
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
|
|
File without changes
|
|
File without changes
|
{instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{instance_repo-1.3.3.dev2 → instance_repo-1.3.5.dev1}/instance_repo.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|