instance-repo 1.3.2.dev2__py3-none-any.whl → 1.3.2.dev4__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.
instance_repo/__init__.py CHANGED
@@ -18,7 +18,7 @@ from .errors import (
18
18
  TransportError,
19
19
  )
20
20
 
21
- __version__ = "1.3.2.dev2"
21
+ __version__ = "1.3.2.dev4"
22
22
 
23
23
  __all__ = [
24
24
  "Repo", "load_instances",
instance_repo/_routing.py CHANGED
@@ -45,14 +45,14 @@ def _split_scheme(endpoint: str) -> tuple[str, str]:
45
45
  return "", endpoint
46
46
 
47
47
 
48
- def _probe_private_endpoint(endpoint: str, timeout: float = 3.0) -> bool:
48
+ def _probe_private_endpoint(endpoint: str, timeout: float = 0.5) -> bool:
49
49
  """Quick TCP connect probe to decide whether a private endpoint is reachable.
50
50
 
51
51
  Performs a raw TCP connect (no TLS handshake) to the endpoint host/port.
52
52
  Returns True if the connect succeeds, False on any error. The default
53
- timeout is 3 seconds — short enough to save 60+ seconds of waiting when
54
- the private endpoint is unreachable, yet long enough for a normal TCP
55
- handshake on a reachable host.
53
+ timeout is 0.5 seconds — 内网 TCP 握手通常在几十毫秒内完成,0.5s 足够
54
+ 判定可达性,同时把公网环境下私网不可达的探测开销从 3s 压到 0.5s
55
+ (结果按 store 缓存 5 分钟,仅端点变更或缓存过期才重探)。
56
56
  """
57
57
  _, host = _split_scheme(endpoint)
58
58
  # Determine port: default 443 for https, 80 for http.
@@ -16,10 +16,13 @@ class DatasetsClient:
16
16
  _BASE = "/apis/v1/datasets/series"
17
17
 
18
18
  def __init__(self, transport, *, content_store=None,
19
- oss_prefix: str = "swe/datasets") -> None:
19
+ oss_prefix: str = "swe/datasets",
20
+ metadata_model: str | None = None) -> None:
20
21
  self._t = transport
21
22
  self._content = content_store
22
23
  self._oss_prefix = oss_prefix
24
+ # OSS 回退探测的布局语义(split_first 默认;version_first 显式切换)
25
+ self._metadata_model = metadata_model or "split_first"
23
26
 
24
27
  def list(self, l1: str | None = None) -> list[DatasetSeries]:
25
28
  """列出 dataset(便利语义:自动翻页取全量,见 :meth:`list_paged` 单页对照)。
@@ -158,7 +161,8 @@ class DatasetsClient:
158
161
  except TransportError as e:
159
162
  if (e.biz_code == 92001 and fallback_to_oss
160
163
  and self._content is not None):
161
- return self._discover_detail(name, storage_env=environment)
164
+ return self._discover_detail(name, storage_env=environment,
165
+ metadata_model=metadata_model)
162
166
  raise
163
167
 
164
168
  def update(self, name: str, *, display_name: str | None = None,
@@ -393,14 +397,16 @@ class DatasetsClient:
393
397
  # ── OSS fallback discovery (private) ─────────────────────────────────
394
398
 
395
399
  def _discover_detail(self, name: str, *,
396
- storage_env: str | None = None) -> DatasetDetail:
400
+ storage_env: str | None = None,
401
+ metadata_model: str | None = None) -> DatasetDetail:
397
402
  from ..store.oss_discovery import (
398
403
  parse_dataset_level, build_synthetic_dataset_detail,
399
404
  _warn_if_truncated)
400
405
  info = parse_dataset_level(
401
- self._content, name, oss_prefix=self._oss_prefix)
406
+ self._content, name, oss_prefix=self._oss_prefix,
407
+ metadata_model=metadata_model or self._metadata_model)
402
408
  _warn_if_truncated(info, name, 500)
403
- if not info.versions:
409
+ if not info.versions and not info.unversioned_splits:
404
410
  from ..errors import DatasetNotFound
405
411
  raise DatasetNotFound(f"dataset {name!r} not found in OSS")
406
412
  return build_synthetic_dataset_detail(
@@ -264,16 +264,23 @@ class SplitsClient:
264
264
  注意:回退场景(dataset 未注册)下 ``version_scope`` 是 MongoDB 语义,
265
265
  OSS 探测无从区分 versioned/unversioned——**忽略 scope 并返回全部探测结果**
266
266
  (G4:否则 ap CLI 无 --version 的 split list 恒空)。``version`` 非空时
267
- 仍精确匹配该 version。
267
+ 仍精确匹配该 version;version 为空时额外包含 split_first 无版本 split
268
+ (version="" 归属)。
268
269
  """
269
270
  from ..store.oss_discovery import (
270
271
  parse_dataset_level, build_synthetic_split,
271
272
  _in_memory_paged, _warn_if_truncated,
272
273
  )
273
274
  info = parse_dataset_level(
274
- self._content, dataset, oss_prefix=self._oss_prefix)
275
+ self._content, dataset, oss_prefix=self._oss_prefix,
276
+ metadata_model=self._discovery_mm())
275
277
  _warn_if_truncated(info, dataset, 500)
276
278
  splits: list[DatasetSplit] = []
279
+ # split_first 无版本布局:dataset 根下直接是 split 目录
280
+ if not version:
281
+ for us in info.unversioned_splits:
282
+ splits.append(build_synthetic_split(
283
+ dataset, "", us.name, instance_count=us.instance_count))
277
284
  for v in info.versions:
278
285
  if version and v.version != version:
279
286
  continue
@@ -295,8 +302,14 @@ class SplitsClient:
295
302
  parse_dataset_level, build_synthetic_split, _warn_if_truncated,
296
303
  )
297
304
  info = parse_dataset_level(
298
- self._content, dataset, oss_prefix=self._oss_prefix)
305
+ self._content, dataset, oss_prefix=self._oss_prefix,
306
+ metadata_model=self._discovery_mm())
299
307
  _warn_if_truncated(info, dataset, 500)
308
+ if not version:
309
+ for us in info.unversioned_splits:
310
+ if us.name == split_name:
311
+ return build_synthetic_split(
312
+ dataset, "", us.name, instance_count=us.instance_count)
300
313
  for v in info.versions:
301
314
  if version and v.version != version:
302
315
  continue
@@ -309,6 +322,13 @@ class SplitsClient:
309
322
  dataset, v.version, split_name)
310
323
  return None
311
324
 
325
+ def _discovery_mm(self) -> str:
326
+ """OSS 回退探测的布局语义(split_first 默认;version_first 显式切换)。"""
327
+ from ..layout import normalize_metadata_model
328
+ return normalize_metadata_model(
329
+ getattr(self._p, "metadata_model", None)
330
+ if self._p is not None else None)
331
+
312
332
  # ── 写 ──
313
333
 
314
334
  def _post_create(self, dataset: str, split: str, version: str,
@@ -229,7 +229,7 @@ class VersionsClient:
229
229
  except TransportError as e:
230
230
  if (e.biz_code == 92001 and fallback_to_oss
231
231
  and self._content is not None):
232
- return self._discover_versions(dataset)
232
+ return self._discover_versions(dataset, metadata_model=metadata_model)
233
233
  raise
234
234
 
235
235
  def list_paged(self, dataset: str, *, status: str | None = None,
@@ -264,7 +264,8 @@ class VersionsClient:
264
264
  if (e.biz_code == 92001 and fallback_to_oss
265
265
  and self._content is not None):
266
266
  return self._discover_versions_paged(
267
- dataset, status, int(page), int(page_size))
267
+ dataset, status, int(page), int(page_size),
268
+ metadata_model=metadata_model)
268
269
  raise
269
270
  rows = data.get("versions", data) if isinstance(data, dict) else data
270
271
  rows = rows if isinstance(rows, list) else []
@@ -295,7 +296,8 @@ class VersionsClient:
295
296
  except TransportError as e:
296
297
  if (e.biz_code == 92001 and fallback_to_oss
297
298
  and self._content is not None):
298
- return self._discover_version(dataset, version)
299
+ return self._discover_version(dataset, version,
300
+ metadata_model=metadata_model)
299
301
  raise
300
302
 
301
303
  def status(self, dataset: str, version: str, *,
@@ -891,31 +893,37 @@ class VersionsClient:
891
893
 
892
894
  # ── OSS fallback discovery (private) ─────────────────────────────────
893
895
 
894
- def _discover_versions(self, dataset: str) -> list:
896
+ def _discover_versions(self, dataset: str,
897
+ metadata_model: str | None = None) -> list:
895
898
  from ..store.oss_discovery import (
896
899
  parse_dataset_level, build_synthetic_version, _warn_if_truncated)
897
900
  info = parse_dataset_level(
898
- self._content, dataset, oss_prefix=self._oss_prefix)
901
+ self._content, dataset, oss_prefix=self._oss_prefix,
902
+ metadata_model=metadata_model or self._discovery_mm())
899
903
  _warn_if_truncated(info, dataset, 500)
900
- if not info.versions:
904
+ # split_first 无版本布局(只有 split)也是发现成功,只是没有版本可列
905
+ if not info.versions and not info.unversioned_splits:
901
906
  from ..errors import DatasetNotFound
902
907
  raise DatasetNotFound(f"dataset {dataset!r} not found in OSS")
903
908
  return [build_synthetic_version(dataset, v) for v in info.versions]
904
909
 
905
910
  def _discover_versions_paged(self, dataset: str, status: str | None,
906
- page: int, page_size: int):
911
+ page: int, page_size: int,
912
+ metadata_model: str | None = None):
907
913
  """OSS 回退的 list_paged:全量探测后按 status 过滤 + 内存分页。"""
908
914
  from ..store.oss_discovery import _in_memory_paged
909
- items = self._discover_versions(dataset)
915
+ items = self._discover_versions(dataset, metadata_model=metadata_model)
910
916
  if (status or "").strip():
911
917
  items = [v for v in items if v.status == status.strip()]
912
918
  return _in_memory_paged(items, int(page), int(page_size))
913
919
 
914
- def _discover_version(self, dataset: str, version: str):
920
+ def _discover_version(self, dataset: str, version: str,
921
+ metadata_model: str | None = None):
915
922
  from ..store.oss_discovery import (
916
923
  parse_dataset_level, build_synthetic_version, _warn_if_truncated)
917
924
  info = parse_dataset_level(
918
- self._content, dataset, oss_prefix=self._oss_prefix)
925
+ self._content, dataset, oss_prefix=self._oss_prefix,
926
+ metadata_model=metadata_model or self._discovery_mm())
919
927
  _warn_if_truncated(info, dataset, 500)
920
928
  for v in info.versions:
921
929
  if v.version == version:
@@ -924,6 +932,13 @@ class VersionsClient:
924
932
  raise DatasetNotFound(
925
933
  f"version {version!r} not found in OSS for dataset {dataset!r}")
926
934
 
935
+ def _discovery_mm(self) -> str:
936
+ """OSS 回退探测的布局语义(split_first 默认;version_first 显式切换)。"""
937
+ from ..layout import normalize_metadata_model
938
+ return normalize_metadata_model(
939
+ getattr(self._p, "metadata_model", None)
940
+ if self._p is not None else None)
941
+
927
942
 
928
943
  # 与 apiserver `DatasetVersionListItem`(pkg/server/dataset_service.go:315)对齐:
929
944
  # 列表项带 instance_count/split_count/updated_at/last_ingested_at,详情接口才带
instance_repo/repo.py CHANGED
@@ -84,7 +84,8 @@ class Repo:
84
84
  self.datasets = DatasetsClient(
85
85
  self.transport,
86
86
  content_store=self._content,
87
- oss_prefix=self.profile.oss_prefix)
87
+ oss_prefix=self.profile.oss_prefix,
88
+ metadata_model=self.profile.metadata_model)
88
89
  self.versions = VersionsClient(
89
90
  self.transport, self.profile,
90
91
  content_store=self._content,
@@ -23,6 +23,7 @@ from ..models import (
23
23
  DatasetInstance,
24
24
  DatasetSplit,
25
25
  DatasetVersion,
26
+ ManifestEntry,
26
27
  PagedResult,
27
28
  SplitManifest,
28
29
  )
@@ -63,6 +64,14 @@ class OssVersionInfo:
63
64
  default_instance_count: int = 0
64
65
 
65
66
 
67
+ @dataclass
68
+ class OssUnversionedSplit:
69
+ """split_first 无版本布局:dataset 根下直接是 split 目录(无 version 层)。"""
70
+ name: str
71
+ has_assets: bool # 同层 {name}-assets/ 存在(split 级 content)
72
+ instance_count: int # {name}/ 下直接 .json 数
73
+
74
+
66
75
  @dataclass
67
76
  class OssDatasetLevelInfo:
68
77
  """Overall dataset structure discovered from OSS directory layout."""
@@ -70,6 +79,12 @@ class OssDatasetLevelInfo:
70
79
  versions: list[OssVersionInfo]
71
80
  total_instance_count: int
72
81
  truncated: bool = False
82
+ # split_first 无版本布局下的 split(version="" 语义)
83
+ unversioned_splits: list[OssUnversionedSplit] = None # type: ignore[assignment]
84
+
85
+ def __post_init__(self):
86
+ if self.unversioned_splits is None:
87
+ self.unversioned_splits = []
73
88
 
74
89
 
75
90
  # ---------------------------------------------------------------------------
@@ -185,11 +200,18 @@ def parse_dataset_level(
185
200
  *,
186
201
  oss_prefix: str = "swe/datasets",
187
202
  max_versions: int = 500,
203
+ metadata_model: str = "split_first",
188
204
  ) -> OssDatasetLevelInfo:
189
205
  """Discover version/split structure from OSS directory layout.
190
206
 
191
- Performs at most ``1 + min(N, max_versions)`` LIST calls where *N* is the
192
- number of Level 1 entries.
207
+ Performs ``1 + min(N, max_versions)`` LIST calls where *N* is the number
208
+ of Level 1 entries; the per-group probes run concurrently (8-way).
209
+
210
+ 语义消歧(``metadata_model``):
211
+ - ``split_first``(SDK 默认):level-1 目录 `X/` 下**只有 .json、无子目录**
212
+ 时,`X` 是无版本 split(`{split}/{id}.json` 契约形态);含子目录时是
213
+ version(子目录为其 split)。
214
+ - ``version_first``(显式切换):level-1 目录一律视为 version(旧行为)。
193
215
 
194
216
  Args:
195
217
  content_store: :class:`~instance_repo.store.oss.OssContentStore` instance.
@@ -197,6 +219,8 @@ def parse_dataset_level(
197
219
  oss_prefix: Root prefix inside the bucket (default ``"swe/datasets"``).
198
220
  max_versions: Upper bound on the number of versions to probe. When
199
221
  exceeded the result is truncated and ``truncated=True`` is set.
222
+ metadata_model: ``split_first``(默认)or ``version_first``;决定
223
+ level-1 目录的语义归属(见上)。
200
224
 
201
225
  Returns:
202
226
  :class:`OssDatasetLevelInfo` with the discovered version/split tree.
@@ -231,90 +255,104 @@ def parse_dataset_level(
231
255
  f"dataset {dataset!r} not found in OSS under {root!r}"
232
256
  )
233
257
 
234
- # -- Group level 1 names into version candidates -----------------------
258
+ # -- Group level 1 names into candidates -------------------------------
235
259
  l1_groups = _group_level_names(level1_names)
236
260
 
237
- if not l1_groups and not level1_json_keys:
261
+ if not l1_groups:
262
+ if level1_json_keys:
263
+ # 实例 .json 直接位于 dataset 根下(无 version/split 层)是**非法布局**:
264
+ # paths.py 契约要求 metadata 至少带 version 层(version_first)或
265
+ # split 层(split_first)。不兼容识别,明确报错引导上传方修正。
266
+ raise DatasetNotFound(
267
+ f"dataset {dataset!r} exists in OSS but has no recognizable "
268
+ f"version/split structure: {len(level1_json_keys)} .json file(s) "
269
+ f"directly under {root!r} (expected {root!r}{{version}}/[{{split}}/] "
270
+ f"layout per paths.py contract)")
238
271
  raise DatasetNotFound(
239
272
  f"dataset {dataset!r} exists in OSS but has no recognizable "
240
273
  f"version/split structure under {root!r}"
241
274
  )
242
275
 
243
276
  # Sort for deterministic output.
244
- version_names = sorted(l1_groups.keys())
277
+ names = sorted(l1_groups.keys())
245
278
 
246
279
  # -- Truncation protection ---------------------------------------------
247
280
  truncated = False
248
- if len(version_names) > max_versions:
249
- version_names = version_names[:max_versions]
281
+ if len(names) > max_versions:
282
+ names = names[:max_versions]
250
283
  truncated = True
251
284
 
252
- # -- Level 2: probe each version ---------------------------------------
253
- versions: list[OssVersionInfo] = []
254
- total_instance_count = 0
255
-
256
- for v_name in version_names:
257
- grp = l1_groups[v_name]
258
- version_dir = f"{root}{v_name}/"
259
-
260
- # 3-layer content/report siblings at level 1.
261
- has_three_layer_content = grp.has_assets # {version}-assets/ exists
285
+ split_first = (metadata_model or "split_first") != "version_first"
262
286
 
263
- # Level 2 listing (subdirectories of {version}/).
287
+ # -- Level 2: probe each candidate (concurrent) ------------------------
288
+ # 每个 group 的完整分析(level-2 列举 + 逐 split 计数)都在 probe 内完成,
289
+ # 使大 version 数数据集的探测全程并行,避免串行逐 split 计数拖慢整链。
290
+ def probe(name: str):
291
+ """分析单个 level-1 group,返回 OssVersionInfo / OssUnversionedSplit。"""
292
+ grp = l1_groups[name]
293
+ group_dir = f"{root}{name}/"
264
294
  try:
265
- level2_names = content_store.list_prefixes(version_dir, list_only=True)
295
+ level2_names, level2_keys = content_store.list_level(
296
+ group_dir, list_only=True)
266
297
  except Exception: # noqa: BLE001
267
- level2_names = []
268
-
298
+ level2_names, level2_keys = [], []
269
299
  l2_groups = _group_level_names(level2_names)
270
-
271
- # Splits are level 2 groups that have a real directory (has_dir) or
272
- # an assets directory (4-layer content pattern).
273
- splits: list[str] = sorted(
274
- name for name, g in l2_groups.items()
275
- if g.has_dir or g.has_assets
276
- )
277
-
278
- # Detect default-split instances: .json files directly under {version}/.
279
- try:
280
- _level2_dirs, v_all_keys = content_store.list_level(
281
- version_dir, list_only=True)
282
- v_json_keys = _direct_json_keys(v_all_keys, version_dir)
283
- has_default_instances = len(v_json_keys) > 0
284
- default_instance_count = len(v_json_keys)
285
- instance_count = len(v_json_keys)
286
- except Exception: # noqa: BLE001
287
- has_default_instances = False
288
- default_instance_count = 0
289
- instance_count = 0
290
-
291
- # Count instances in each named split directory.
300
+ direct_json = _direct_json_keys(level2_keys, group_dir)
301
+ subdirs = [g for g in l2_groups.values() if g.has_dir or g.has_assets]
302
+
303
+ # split_first 无版本布局:`{name}/` 下只有 .json、无子目录 →
304
+ # 无版本 split(contract: {split}/{id}.json + {split}-assets/)。
305
+ if split_first and grp.has_dir and not subdirs and direct_json:
306
+ return OssUnversionedSplit(
307
+ name=name,
308
+ has_assets=grp.has_assets,
309
+ instance_count=len(direct_json),
310
+ )
311
+
312
+ # version 路径(version_first 或含子目录的 split_first 形态)。
313
+ splits: list[str] = sorted(g.name for g in subdirs)
314
+ instance_count = len(direct_json)
292
315
  for s_name in splits:
293
- split_dir = f"{version_dir}{s_name}/"
316
+ split_dir = f"{group_dir}{s_name}/"
294
317
  try:
295
318
  _s_dirs, s_keys = content_store.list_level(
296
319
  split_dir, list_only=True)
297
- s_json = _direct_json_keys(s_keys, split_dir)
298
- instance_count += len(s_json)
320
+ instance_count += len(_direct_json_keys(s_keys, split_dir))
299
321
  except Exception: # noqa: BLE001
300
322
  pass # best-effort
301
-
302
- total_instance_count += instance_count
303
-
304
- versions.append(OssVersionInfo(
305
- version=v_name,
306
- has_three_layer_content=has_three_layer_content,
323
+ return OssVersionInfo(
324
+ version=name,
325
+ has_three_layer_content=grp.has_assets,
307
326
  splits=splits,
308
- has_default_instances=has_default_instances,
327
+ has_default_instances=len(direct_json) > 0,
309
328
  instance_count=instance_count,
310
- default_instance_count=default_instance_count,
311
- ))
329
+ default_instance_count=len(direct_json),
330
+ )
331
+
332
+ try:
333
+ from ..concurrency import map_concurrent
334
+ results = map_concurrent(probe, names, concurrency=16)
335
+ except Exception: # noqa: BLE001 - 并发不可用时退化为串行
336
+ results = [probe(n) for n in names]
337
+
338
+ versions: list[OssVersionInfo] = []
339
+ unversioned_splits: list[OssUnversionedSplit] = []
340
+ total_instance_count = 0
341
+
342
+ for entry in results:
343
+ if isinstance(entry, OssUnversionedSplit):
344
+ unversioned_splits.append(entry)
345
+ total_instance_count += entry.instance_count
346
+ else:
347
+ versions.append(entry)
348
+ total_instance_count += entry.instance_count
312
349
 
313
350
  return OssDatasetLevelInfo(
314
351
  dataset=dataset,
315
352
  versions=versions,
316
353
  total_instance_count=total_instance_count,
317
354
  truncated=truncated,
355
+ unversioned_splits=unversioned_splits,
318
356
  )
319
357
 
320
358
 
@@ -323,9 +361,10 @@ def _warn_if_truncated(info: OssDatasetLevelInfo, dataset: str, max_versions: in
323
361
  if info.truncated:
324
362
  import logging
325
363
  logging.getLogger(__name__).warning(
326
- "dataset %r has more than %d versions; OSS discovery results "
364
+ "dataset %r has more than %d level-1 entries; OSS discovery results "
327
365
  "are truncated (showing %d of >%d)",
328
- dataset, max_versions, len(info.versions), max_versions)
366
+ dataset, max_versions,
367
+ len(info.versions) + len(info.unversioned_splits), max_versions)
329
368
 
330
369
 
331
370
  # ---------------------------------------------------------------------------
@@ -389,6 +428,10 @@ def build_synthetic_dataset_detail(
389
428
  if v.has_default_instances:
390
429
  all_splits.add("default")
391
430
  total_instances += v.instance_count
431
+ # split_first 无版本布局:level-1 目录即 split
432
+ for us in info.unversioned_splits:
433
+ all_splits.add(us.name)
434
+ total_instances += us.instance_count
392
435
 
393
436
  available_envs: list[str] = [storage_env] if storage_env else []
394
437
 
@@ -450,10 +493,11 @@ def build_synthetic_split(
450
493
  instance_count: int = 0,
451
494
  ) -> DatasetSplit:
452
495
  """Build a :class:`DatasetSplit` from OSS discovery (``registered=False``)."""
496
+ base = f"swe/datasets/{dataset}" + (f"/{version}" if version else "")
453
497
  if split_name == "default":
454
- storage_path = f"swe/datasets/{dataset}/{version}/"
498
+ storage_path = f"{base}/"
455
499
  else:
456
- storage_path = f"swe/datasets/{dataset}/{version}/{split_name}/"
500
+ storage_path = f"{base}/{split_name}/"
457
501
 
458
502
  return DatasetSplit(
459
503
  dataset=dataset,
@@ -468,6 +512,16 @@ def build_synthetic_split(
468
512
  )
469
513
 
470
514
 
515
+ def _metadata_prefix(dataset: str, version: str, split: str | None,
516
+ oss_prefix: str) -> str:
517
+ """拼接 metadata 列举前缀;version 为空时表示 split_first 无版本布局。"""
518
+ l1, l2 = P.split_dataset(dataset)
519
+ base = f"{oss_prefix}/{l1}/{l2}" + (f"/{version}" if version else "")
520
+ if split and split != "default":
521
+ return f"{base}/{split}/"
522
+ return f"{base}/"
523
+
524
+
471
525
  def build_synthetic_instances(
472
526
  content_store,
473
527
  dataset: str,
@@ -475,20 +529,16 @@ def build_synthetic_instances(
475
529
  split: str | None = None,
476
530
  *,
477
531
  oss_prefix: str = "swe/datasets",
532
+ read_content: bool = False,
478
533
  ) -> list[DatasetInstance]:
479
534
  """List and build :class:`DatasetInstance` objects from OSS metadata JSONs.
480
535
 
481
536
  For each ``{instance_id}.json`` found directly under the target prefix, a
482
- ``DatasetInstance`` is constructed. An attempt is made to read the JSON
483
- body to populate ``tags``, ``difficulty``, ``docker_image``, and
484
- ``extend_meta``; on failure the instance is returned with those fields
485
- left at their defaults (best-effort).
537
+ ``DatasetInstance`` is constructed. ``read_content=True`` 时尝试读 JSON
538
+ 回填 tags/difficulty/docker_image;默认 False——发现模式(list-only STS)
539
+ 无 GetObject,逐实例 GET 必然失败,跳过可省 N 次无谓的 STS 签发与下载。
486
540
  """
487
- l1, l2 = P.split_dataset(dataset)
488
- if split and split != "default":
489
- metadata_prefix = f"{oss_prefix}/{l1}/{l2}/{version}/{split}/"
490
- else:
491
- metadata_prefix = f"{oss_prefix}/{l1}/{l2}/{version}/"
541
+ metadata_prefix = _metadata_prefix(dataset, version, split, oss_prefix)
492
542
 
493
543
  all_keys = content_store.list_objects(metadata_prefix, list_only=True)
494
544
  json_keys = _direct_json_keys(all_keys, metadata_prefix)
@@ -503,20 +553,74 @@ def build_synthetic_instances(
503
553
  split=split or "default",
504
554
  registered=False,
505
555
  )
506
- # Best-effort: read JSON to populate optional fields. With list_only=True
507
- # STS, GetObject is not granted, so these reads will fail silently (returning
508
- # empty dicts) — the instance will still be listed but with empty optional fields.
509
- data = _read_json_object(content_store, key)
510
- if data:
511
- inst.tags = data.get("tags", {})
512
- inst.difficulty = data.get("difficulty")
513
- inst.docker_image = data.get("docker_image")
514
- inst.extend_meta = data.get("extend_meta", {})
556
+ if read_content:
557
+ data = _read_json_object(content_store, key)
558
+ if data:
559
+ inst.tags = data.get("tags", {})
560
+ inst.difficulty = data.get("difficulty")
561
+ inst.docker_image = data.get("docker_image")
562
+ inst.extend_meta = data.get("extend_meta", {})
515
563
  instances.append(inst)
516
564
 
517
565
  return instances
518
566
 
519
567
 
568
+ def _instance_dir(dataset: str, version: str, split: str | None,
569
+ instance_id: str, suffix: str, oss_prefix: str) -> str:
570
+ """拼接实例的 -assets/-report 目录;version 为空表示无版本 split 布局。"""
571
+ l1, l2 = P.split_dataset(dataset)
572
+ base = f"{oss_prefix}/{l1}/{l2}" + (f"/{version}" if version else "")
573
+ if split and split != "default":
574
+ return f"{base}/{split}-{suffix}/{instance_id}/"
575
+ return f"{base}-{suffix}/{instance_id}/"
576
+
577
+
578
+ def _format_from_key(key: str) -> str:
579
+ if key.endswith(".jsonl"):
580
+ return "jsonl"
581
+ if key.endswith(".json"):
582
+ return "json"
583
+ if key.endswith(".tgz") or key.endswith(".tar.gz"):
584
+ return "tgz"
585
+ return ""
586
+
587
+
588
+ def _instance_manifest(
589
+ content_store,
590
+ dataset: str,
591
+ version: str,
592
+ instance_id: str,
593
+ split: str | None,
594
+ metadata_key: str,
595
+ *,
596
+ oss_prefix: str,
597
+ ) -> list[ManifestEntry]:
598
+ """用 LIST 枚举实例的文件结构(metadata + {split}-assets + {split}-report)。
599
+
600
+ 发现模式(list-only STS)无 GetObject,读不到 metadata JSON 内容,但目录
601
+ 结构可通过 LIST 完整呈现——恰好满足「查看实例的目录文件结构」需求。
602
+ """
603
+ keys = [metadata_key]
604
+ for suffix in ("assets", "report"):
605
+ try:
606
+ keys.extend(k for k in content_store.list_objects(
607
+ _instance_dir(dataset, version, split, instance_id,
608
+ suffix, oss_prefix),
609
+ list_only=True) if not k.endswith("/"))
610
+ except Exception: # noqa: BLE001
611
+ pass # best-effort:无 assets/report 目录时忽略
612
+ bucket = getattr(content_store, "bucket_name", "") or ""
613
+ return [
614
+ ManifestEntry(
615
+ file_path=f"oss://{bucket}/{k}" if bucket else k,
616
+ content_type="file",
617
+ content_format=_format_from_key(k),
618
+ object_path=k,
619
+ )
620
+ for k in keys
621
+ ]
622
+
623
+
520
624
  def build_synthetic_instance(
521
625
  content_store,
522
626
  dataset: str,
@@ -525,13 +629,30 @@ def build_synthetic_instance(
525
629
  split: str | None = None,
526
630
  *,
527
631
  oss_prefix: str = "swe/datasets",
632
+ read_content: bool = False,
633
+ include_files: bool = True,
528
634
  ) -> DatasetInstance | None:
529
635
  """Build a single :class:`DatasetInstance` by exact key lookup in OSS.
530
636
 
531
- Returns ``None`` if the metadata JSON does not exist.
637
+ Returns ``None`` if the metadata JSON does not exist. ``read_content``
638
+ 语义同 :func:`build_synthetic_instances`(发现模式默认跳过 JSON 回填)。
639
+
640
+ ``include_files``(默认 True)用 LIST 枚举实例文件结构(metadata + assets
641
+ + report 目录)回填 ``manifest``——文件名为真实值,digest/大小留空
642
+ (list-only STS 无 GetObject)。仅 instance get 单条路径开启,避免列表
643
+ 路径 N 倍 LIST 开销。
532
644
  """
533
- key = P.metadata_key(
534
- dataset, version, instance_id, split, prefix=oss_prefix)
645
+ if version:
646
+ key = P.metadata_key(
647
+ dataset, version, instance_id, split, prefix=oss_prefix)
648
+ else:
649
+ # split_first 无版本布局:{split}/{id}.json(split=None/default 不合
650
+ # 此形态,调用方须传真实 split 名)
651
+ l1, l2 = P.split_dataset(dataset)
652
+ base = f"{oss_prefix}/{l1}/{l2}"
653
+ key = (f"{base}/{split}/{instance_id}.json"
654
+ if split and split != "default"
655
+ else f"{base}/{instance_id}.json")
535
656
 
536
657
  # 存在性判定用 LIST(list_only=True 仅需 ListObjects)而非 HEAD——
537
658
  # list-only STS 无 oss:GetObject,object_exists 的 HEAD 会在 STS 签发或
@@ -551,11 +672,19 @@ def build_synthetic_instance(
551
672
  split=split or "default",
552
673
  registered=False,
553
674
  )
554
- data = _read_json_object(content_store, key)
555
- if data:
556
- inst.tags = data.get("tags", {})
557
- inst.difficulty = data.get("difficulty")
558
- inst.docker_image = data.get("docker_image")
559
- inst.extend_meta = data.get("extend_meta", {})
675
+ if include_files:
676
+ try:
677
+ inst.manifest = _instance_manifest(
678
+ content_store, dataset, version, instance_id, split, key,
679
+ oss_prefix=oss_prefix)
680
+ except Exception: # noqa: BLE001
681
+ pass # best-effort:manifest 枚举失败不阻断实例返回
682
+ if read_content:
683
+ data = _read_json_object(content_store, key)
684
+ if data:
685
+ inst.tags = data.get("tags", {})
686
+ inst.difficulty = data.get("difficulty")
687
+ inst.docker_image = data.get("docker_image")
688
+ inst.extend_meta = data.get("extend_meta", {})
560
689
 
561
690
  return inst
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: instance-repo
3
- Version: 1.3.2.dev2
3
+ Version: 1.3.2.dev4
4
4
  Summary: InstanceRepo SDK — 评测 Instance 统一存储客户端(SDK-only;控制面由 apiserver 承载)
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1,5 +1,5 @@
1
- instance_repo/__init__.py,sha256=ae2kTZekBd48C9oAOBgdUsN2JST5lkCSA6M348urFGI,1422
2
- instance_repo/_routing.py,sha256=XbdKPF-Zh2sEkr4UyLC9HuiRcTLP-ssvhp6SCDC98YM,8326
1
+ instance_repo/__init__.py,sha256=lAyyofR-xkJ6HHVt7KUbO5Ar3_hFdKTJxyAK13u7hnE,1422
2
+ instance_repo/_routing.py,sha256=fX3oKJNca1XTD1pKK304eq0clf7I8iEdFkd2WFO3oV4,8414
3
3
  instance_repo/_site_defaults.py,sha256=ck7aHHTup8ibBo0l5MSR03XpTtxJ11hYo7oM2DzWs4A,2383
4
4
  instance_repo/artifacts.py,sha256=OOcSI1ehB_dw1ZNScjyhzghPqOdL534kQp_iI2Hz7IU,10659
5
5
  instance_repo/bootstrap.py,sha256=qtBZoq77zHUcri3EgUleTrXTDUQq3GU9cI2wzw_wly4,9658
@@ -15,7 +15,7 @@ instance_repo/loader.py,sha256=lWjhw0MjEIIxbZ5w6cjyaDIWkNVFDhjjNdu1lc5zRuo,474
15
15
  instance_repo/models.py,sha256=-DyP2DBu2vNecmASbyExfNu5nJkutNsqWInhlq8_ZMU,35987
16
16
  instance_repo/paths.py,sha256=EL0d613T0mnabiYCtZQBC50rZT3mWKUq-3TepPbTRec,8438
17
17
  instance_repo/release.py,sha256=Up96Qeh2GRH26BSAHVEjDFQnFCaEA-vIvSme3a8FQFo,13882
18
- instance_repo/repo.py,sha256=5D7jYLeaDSZLKnf548YlfXT9tUPNgdcXT2j31wzShpA,40305
18
+ instance_repo/repo.py,sha256=XDa-61in_7P-2SGIMDbNM1okbJYuJh6ODlQCdY1DajI,40361
19
19
  instance_repo/retry.py,sha256=8y-AX9qmnLqGwqRzVJ5PKqZt2fWyqUL2t2P0suSBAA4,3438
20
20
  instance_repo/tasktoml.py,sha256=b-tWfNiOHN9D-ixCbPpCLnZaaU9Q6LRGG0Wl4YN8r10,5152
21
21
  instance_repo/transport.py,sha256=2Vh2atYxmq5Hrh0mc-10yA3p8QUOQruAE_2f1i1uwWk,21995
@@ -23,22 +23,22 @@ instance_repo/validate.py,sha256=NMR-vFBIU_FQ0WKdBZuQ-ua4jtkAzvWwEjnAHyAFwDs,118
23
23
  instance_repo/clients/__init__.py,sha256=5V7Pe0b6nnMiwIhACA4ctQowovrL4iHTiHKKcjmsBQo,488
24
24
  instance_repo/clients/benchmarks.py,sha256=RzHY8VzDP1o2jT4M8gxxbcr3amuNr4mlSblZhhJqG_w,2778
25
25
  instance_repo/clients/config.py,sha256=UdfNDqeQnFaOBBAl-Aj-W2JvuouvJ11q-_gB4da9yBg,21209
26
- instance_repo/clients/datasets.py,sha256=0b94qr-UNXKuEg1zIi3dl1FKd0Rz0TokvlflNdeUL88,24052
26
+ instance_repo/clients/datasets.py,sha256=snSqr4tHVzOCj4HnQlGHoKmqtz1HrxEoMNoja4yrSuI,24497
27
27
  instance_repo/clients/images.py,sha256=8i0FXxAAL2mOUUTrVgDdWSYM2imJ5nOEXzpVRBa-G-k,4911
28
28
  instance_repo/clients/instances.py,sha256=IOAKl3MAlW3UcftX8b5cGIEdC5_1qfouaVSE_0-OAxE,58125
29
29
  instance_repo/clients/reports.py,sha256=4Z8bpcSi-mmfkQLKpMklelLI4mgHcnjfb0725bE2nkI,3001
30
30
  instance_repo/clients/scaffold.py,sha256=VLicNFnqKi2iYB-gjOkQ9wa1unNOJXO7UxBZlz7El5A,4998
31
- instance_repo/clients/splits.py,sha256=9hUZAiLCv0gZpxbEPvxtsJZ0qeGxSxaYl21ARF3sl58,27211
32
- instance_repo/clients/versions.py,sha256=jeLwV0K5h4lpeeEMPQMnv-BpzfRcm0jiqcGIa0Nl2RI,52658
31
+ instance_repo/clients/splits.py,sha256=o8ygCVAMrxECNojzfUdaHQtuwL4Imzm7ghNKWPxG-VE,28255
32
+ instance_repo/clients/versions.py,sha256=IOzGckKjQGTWC4w1Zqg12hN6bdzX2gClOZdgNCHdeyA,53639
33
33
  instance_repo/clients/workflows.py,sha256=6ftl1tuyEGx8BtMLSjfExP9_VcQhK4veo7ex1jDOdlc,3686
34
34
  instance_repo/store/__init__.py,sha256=fGwePNjKuojqx1yagSaY-svONE5pjaEz2BZmG0ISUZ0,674
35
35
  instance_repo/store/acr.py,sha256=OD7LH3GZY4HrrbEht86Mf3NfW_lh-8MhCTOGyX6gLT4,19468
36
36
  instance_repo/store/base.py,sha256=7feHdDXDvSkq60wXYvF6p8TK9KRj7YTLlESdgZ5MQHo,2622
37
37
  instance_repo/store/oss.py,sha256=-1dBPlUBLD0z-aI4GOuO6kwGyIgZX1zRvYCD-IPp-Zs,30672
38
- instance_repo/store/oss_discovery.py,sha256=4s8n2i8AhiNCKP2LoDaRgELEOLW3quodRgREhMM0I3g,20008
38
+ instance_repo/store/oss_discovery.py,sha256=gFZmHkpl1136hsXGL2CEBcHEbnpbTz3kAt2OZA71U-E,25894
39
39
  instance_repo/store/registry.py,sha256=1R2GqXsuDZ8wc3IGpncF9yL6IHk-FeiwUlZN7v2ndSc,10837
40
- instance_repo-1.3.2.dev2.dist-info/METADATA,sha256=H2v_y6ot4xb5UX5sY5YwDfOgEIt96jdDLxPO8NQXVvg,14227
41
- instance_repo-1.3.2.dev2.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
42
- instance_repo-1.3.2.dev2.dist-info/entry_points.txt,sha256=tn6jgwYChSx7eAqw9YNed_JFYYgoRtE7WkFn8xqa8Rw,53
43
- instance_repo-1.3.2.dev2.dist-info/top_level.txt,sha256=mblK0Bkwwp6jCZioS6q3sSuFFs8NR9DKTJ0DJpIRDmY,14
44
- instance_repo-1.3.2.dev2.dist-info/RECORD,,
40
+ instance_repo-1.3.2.dev4.dist-info/METADATA,sha256=CY7IFFYrk54y2DSipSc5lSGMsFgGQfZiHfdh2ykx518,14227
41
+ instance_repo-1.3.2.dev4.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
42
+ instance_repo-1.3.2.dev4.dist-info/entry_points.txt,sha256=tn6jgwYChSx7eAqw9YNed_JFYYgoRtE7WkFn8xqa8Rw,53
43
+ instance_repo-1.3.2.dev4.dist-info/top_level.txt,sha256=mblK0Bkwwp6jCZioS6q3sSuFFs8NR9DKTJ0DJpIRDmY,14
44
+ instance_repo-1.3.2.dev4.dist-info/RECORD,,