instance-repo 1.6.0.dev0__tar.gz → 1.6.0.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/PKG-INFO +1 -1
  2. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/__init__.py +1 -1
  3. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/instances.py +100 -3
  4. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/oss.py +69 -0
  5. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/PKG-INFO +1 -1
  6. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/pyproject.toml +1 -1
  7. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/README.md +0 -0
  8. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/_routing.py +0 -0
  9. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/_site_defaults.py +0 -0
  10. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/artifacts.py +0 -0
  11. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/bootstrap.py +0 -0
  12. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/cache.py +0 -0
  13. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/cli.py +0 -0
  14. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/__init__.py +0 -0
  15. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/_rfc3339.py +0 -0
  16. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/ap_tool.py +0 -0
  17. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/benchmarks.py +0 -0
  18. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/config.py +0 -0
  19. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/datasets.py +0 -0
  20. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/images.py +0 -0
  21. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/reports.py +0 -0
  22. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/scaffold.py +0 -0
  23. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/shared.py +0 -0
  24. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/splits.py +0 -0
  25. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_acl.py +0 -0
  26. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_directories.py +0 -0
  27. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_entries.py +0 -0
  28. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_reads.py +0 -0
  29. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_upload.py +0 -0
  30. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_writes.py +0 -0
  31. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/versions.py +0 -0
  32. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/workflows.py +0 -0
  33. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/concurrency.py +0 -0
  34. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/content.py +0 -0
  35. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/endpoints.py +0 -0
  36. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/errors.py +0 -0
  37. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/image.py +0 -0
  38. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/layout.py +0 -0
  39. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/loader.py +0 -0
  40. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/models.py +0 -0
  41. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/paths.py +0 -0
  42. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/release.py +0 -0
  43. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/repo.py +0 -0
  44. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/retry.py +0 -0
  45. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/__init__.py +0 -0
  46. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/acr.py +0 -0
  47. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/base.py +0 -0
  48. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/oss_discovery.py +0 -0
  49. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/registry.py +0 -0
  50. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/tasktoml.py +0 -0
  51. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/transport.py +0 -0
  52. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/validate.py +0 -0
  53. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/SOURCES.txt +0 -0
  54. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/dependency_links.txt +0 -0
  55. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/entry_points.txt +0 -0
  56. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/requires.txt +0 -0
  57. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/top_level.txt +0 -0
  58. {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: instance-repo
3
- Version: 1.6.0.dev0
3
+ Version: 1.6.0.dev2
4
4
  Summary: InstanceRepo SDK — 评测 Instance 统一存储客户端(SDK-only;控制面由 apiserver 承载)
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -20,7 +20,7 @@ from .errors import (
20
20
  RegistrationFailed, TransportError,
21
21
  )
22
22
 
23
- __version__ = "1.6.0.dev0"
23
+ __version__ = "1.6.0.dev2"
24
24
 
25
25
  __all__ = [
26
26
  "Repo", "load_instances",
@@ -571,10 +571,19 @@ class InstancesClient:
571
571
  def delete(self, dataset: str, version: str, instance_id: str, *,
572
572
  split: str | None = None,
573
573
  environment: str | None = None,
574
- metadata_model: str | None = None) -> None:
574
+ metadata_model: str | None = None,
575
+ delete_files: bool = False) -> None:
575
576
  """删除单个实例的元数据记录(``DELETE /datasets/instances/detail``)。
576
577
 
577
- 仅删除 MongoDB 记录,不删除 OSS 对象。后续 ingest 扫描可能重建该记录。
578
+ 默认仅删除 MongoDB 记录,后续 ingest 可能重建记录。
579
+
580
+ ``delete_files=True`` 显式删除 canonical split-first 布局下该实例的
581
+ JSON、assets 和 report 子树,然后删除数据库记录。不会删除 ACR 镜像或
582
+ OSS 历史版本。仅走私网,要求服务端签发 ListObjects/DeleteObject 权限;
583
+ SDK 不授予或推断 Admin/Writer 权限,实际权限以服务端和 OSS 为准。
584
+ OSS 失败时保留数据库记录;数据库失败时文件可能已删,原参数可重试。
585
+ 此操作不具备跨 OSS/数据库事务性,调用方必须先停止同实例的上传/ingest。
586
+ 非 canonical storage_path 不支持文件删除,不能用 manifest 扩大删除范围。
578
587
  """
579
588
  from urllib.parse import quote
580
589
  if not (dataset or "").strip():
@@ -598,8 +607,96 @@ class InstancesClient:
598
607
  requested_model = getattr(self._p, "metadata_model", None)
599
608
  params.append(("metadata_model",
600
609
  business_metadata_model(requested_model)))
610
+ if not isinstance(delete_files, bool):
611
+ raise SchemaError("delete_files must be a bool")
612
+ if delete_files:
613
+ self._delete_instance_files(
614
+ dataset.strip(), (version or "").strip(), instance_id.strip(),
615
+ split=split, environment=env,
616
+ metadata_model=requested_model)
601
617
  qs = "&".join(f"{k}={quote(v, safe='')}" for k, v in params)
602
- self._t.delete(f"/apis/v1/datasets/instances/detail?{qs}")
618
+ failure = None
619
+ try:
620
+ self._t.delete(f"/apis/v1/datasets/instances/detail?{qs}")
621
+ except Exception as exc:
622
+ if not delete_files:
623
+ raise
624
+ status = getattr(exc, "status", None)
625
+ code = getattr(exc, "biz_code", None)
626
+ failure = TransportError(
627
+ "OSS files were removed but instance metadata deletion failed; "
628
+ "retry the same deletion after resolving server authorization or availability",
629
+ status=status if isinstance(status, int) else None,
630
+ biz_code=code if isinstance(code, int) else None)
631
+ # Raise after leaving the raw error handler: ``from None`` inside it
632
+ # hides the traceback but still retains signed URLs in __context__.
633
+ if failure is not None:
634
+ raise failure from None
635
+
636
+ def _delete_instance_files(self, dataset: str, version: str, instance_id: str, *,
637
+ split: str | None, environment: str,
638
+ metadata_model: str | None) -> None:
639
+ """Validate the complete locator before either storage or metadata mutation."""
640
+ lay = self._layout(metadata_model)
641
+ P.DatasetName.parse(dataset)
642
+ validate_instance_id(instance_id)
643
+ # Reject aliases/traversal instead of letting Layout strip slashes.
644
+ for name, value in (("version", version), ("split", split)):
645
+ if name == "version" and not value:
646
+ continue
647
+ if not isinstance(value, str) or value != value.strip():
648
+ raise SchemaError(f"{name} must be a canonical path segment")
649
+ validate_instance_id(value)
650
+ P.DatasetName.parse(lay.prefix)
651
+ bound_env = (getattr(self._p, "dataset_environment", "") or "").strip()
652
+ if not bound_env or environment != bound_env:
653
+ raise SchemaError(
654
+ "file deletion requires environment to match the storage profile")
655
+ bucket = getattr(self._p, "oss_bucket", "")
656
+ if not bucket or getattr(self._content, "bucket_name", None) != bucket:
657
+ raise SchemaError("file deletion requires a matching OSS content store")
658
+ operation = getattr(self._content, "delete_instance", None)
659
+ if not callable(operation):
660
+ raise SchemaError("content store does not support instance file deletion")
661
+ # Read the registered record without OSS discovery. A custom storage_path
662
+ # must never turn a canonical-layout delete into deletion of other bytes.
663
+ meta = self.get(dataset, version, instance_id, split,
664
+ environment=environment, metadata_model=lay.model,
665
+ fallback_to_oss=False)
666
+ if meta is None or (
667
+ meta.dataset, meta.dataset_version, meta.instance_id, meta.split
668
+ ) != (dataset, version, instance_id, split):
669
+ raise SchemaError("file deletion requires the matching registered instance")
670
+ main = lay.metadata_key(dataset, instance_id, version=version, split=split)
671
+ assets = lay.assets_dir(dataset, instance_id, version=version, split=split)
672
+ report = lay.report_key(dataset, instance_id, version=version, split=split)
673
+ main_uri = f"oss://{bucket}/{main}"
674
+ allowed = (f"oss://{bucket}/{assets}",
675
+ f"oss://{bucket}/{report.rsplit('/', 1)[0]}/")
676
+ if not any(entry.file_path == main_uri for entry in meta.manifest):
677
+ raise SchemaError("file deletion requires a canonical metadata JSON manifest")
678
+ for entry in meta.manifest:
679
+ uri = entry.file_path
680
+ if not isinstance(uri, str) or not (
681
+ uri == main_uri or any(
682
+ uri == prefix.rstrip("/") or uri.startswith(prefix)
683
+ for prefix in allowed)):
684
+ raise SchemaError(
685
+ "file deletion only supports canonical instance manifests")
686
+ failure = None
687
+ try:
688
+ operation(dataset, version, instance_id, split=split, layout=lay)
689
+ except Exception as exc:
690
+ # Do not propagate OSS exceptions containing signed URLs/credentials.
691
+ status = getattr(exc, "status", None)
692
+ failure = TransportError(
693
+ "instance file deletion failed; metadata was not deleted; "
694
+ "check server-issued ListObjects/DeleteObject permission and "
695
+ "private connectivity; files may be partially removed; "
696
+ "stop concurrent writers before retrying",
697
+ status=status if isinstance(status, int) else None)
698
+ if failure is not None:
699
+ raise failure from None
603
700
 
604
701
  def update(self, dataset: str, version: str, instance_id: str, *,
605
702
  split: str | None = None, dataset_split_id: str | None = None,
@@ -485,6 +485,75 @@ class OssContentStore(BaseContentStore):
485
485
  _OperationIntent.NON_DOWNLOAD, prefix, dataset, list_all,
486
486
  list_only=list_only)
487
487
 
488
+ def delete_instance(self, dataset: str, version: str, instance_id: str, *,
489
+ split: str, layout) -> None:
490
+ """Delete only one canonical instance, using server-issued STS privately.
491
+
492
+ Per subtree: at most 1000 pages of 200 objects, O(200) memory, sequential
493
+ requests and existing bounded transport retries. Stop writers/ingesters
494
+ before calling; this is not an atomic delete or a version-history purge.
495
+ """
496
+ from ..errors import SchemaError
497
+ from ..validate import validate_instance_id
498
+
499
+ P.DatasetName.parse(dataset)
500
+ P.DatasetName.parse(layout.prefix)
501
+ for value in (instance_id, split):
502
+ validate_instance_id(value)
503
+ if version:
504
+ validate_instance_id(version)
505
+ scope = layout.sts_prefix(dataset)
506
+ metadata = layout.metadata_key(dataset, instance_id, version=version, split=split)
507
+ prefixes = (
508
+ layout.assets_dir(dataset, instance_id, version=version, split=split),
509
+ layout.report_key(dataset, instance_id, version=version, split=split)
510
+ .rsplit("/", 1)[0] + "/",
511
+ )
512
+ if not all(key.startswith(scope) for key in (metadata, *prefixes)):
513
+ raise SchemaError("instance deletion escaped the dataset root")
514
+
515
+ def remove(list_bucket, delete_bucket):
516
+ for prefix in prefixes:
517
+ marker = ""
518
+ for _ in range(1000):
519
+ page = self._do(lambda: list_bucket.list_objects(
520
+ prefix=prefix, marker=marker, max_keys=200))
521
+ objects = list(page.object_list)
522
+ if len(objects) > 200:
523
+ raise SchemaError("OSS deletion page exceeds the requested limit")
524
+ keys = [obj.key for obj in objects]
525
+ if any(not isinstance(key, str) or not key.startswith(prefix)
526
+ or (marker and key <= marker) for key in keys):
527
+ raise SchemaError("OSS deletion listing escaped its instance subtree")
528
+ next_marker = page.next_marker
529
+ if page.is_truncated and (
530
+ not isinstance(next_marker, str)
531
+ or not next_marker.startswith(prefix)
532
+ or next_marker <= marker
533
+ or (keys and next_marker < max(keys))):
534
+ raise SchemaError("OSS deletion listing returned an invalid marker")
535
+ for key in keys:
536
+ self._do(lambda key=key: delete_bucket.delete_object(key))
537
+ if not page.is_truncated:
538
+ break
539
+ marker = next_marker
540
+ else:
541
+ raise SchemaError("OSS deletion page budget exceeded; retry after quiescing writers")
542
+ # Remove the ingest source only after both scoped subtrees succeed.
543
+ self._do(lambda: delete_bucket.delete_object(metadata))
544
+
545
+ # Reuse the existing strict private-endpoint path (also used by downloads),
546
+ # without the public fallback used for ordinary non-download operations.
547
+ # Listing and object mutation are distinct grants on the server. Do not
548
+ # assume a normal writer/admin token includes ListObjects, or try to
549
+ # mutate with the discovery-only token.
550
+ list_bucket = self._execute_oss(
551
+ _OperationIntent.DOWNLOAD, scope, dataset, lambda bucket: bucket,
552
+ list_only=True)
553
+ delete_bucket = self._execute_oss(
554
+ _OperationIntent.DOWNLOAD, scope, dataset, lambda bucket: bucket)
555
+ remove(list_bucket, delete_bucket)
556
+
488
557
  def list_prefixes(self, prefix: str, *, list_only: bool = False,
489
558
  dataset: str | None = None) -> list[str]:
490
559
  """List immediate child *directories* under *prefix* using ``delimiter='/'``.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: instance-repo
3
- Version: 1.6.0.dev0
3
+ Version: 1.6.0.dev2
4
4
  Summary: InstanceRepo SDK — 评测 Instance 统一存储客户端(SDK-only;控制面由 apiserver 承载)
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "instance-repo"
7
- version = "1.6.0.dev0"
7
+ version = "1.6.0.dev2"
8
8
  description = "InstanceRepo SDK — 评测 Instance 统一存储客户端(SDK-only;控制面由 apiserver 承载)"
9
9
  requires-python = ">=3.10"
10
10
  readme = "README.md"