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.
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/PKG-INFO +1 -1
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/__init__.py +1 -1
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/instances.py +100 -3
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/oss.py +69 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/PKG-INFO +1 -1
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/pyproject.toml +1 -1
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/README.md +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/_routing.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/_site_defaults.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/artifacts.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/bootstrap.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/cache.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/cli.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/__init__.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/_rfc3339.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/ap_tool.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/benchmarks.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/config.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/datasets.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/images.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/reports.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/scaffold.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/shared.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/splits.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_acl.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_directories.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_entries.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_reads.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_upload.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_writes.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/versions.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/workflows.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/concurrency.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/content.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/endpoints.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/errors.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/image.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/layout.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/loader.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/models.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/paths.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/release.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/repo.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/retry.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/__init__.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/acr.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/base.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/oss_discovery.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/registry.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/tasktoml.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/transport.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/validate.py +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/SOURCES.txt +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/dependency_links.txt +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/entry_points.txt +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/requires.txt +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/top_level.txt +0 -0
- {instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/setup.cfg +0 -0
|
@@ -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
|
|
574
|
+
metadata_model: str | None = None,
|
|
575
|
+
delete_files: bool = False) -> None:
|
|
575
576
|
"""删除单个实例的元数据记录(``DELETE /datasets/instances/detail``)。
|
|
576
577
|
|
|
577
|
-
|
|
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
|
-
|
|
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='/'``.
|
|
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.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_acl.py
RENAMED
|
File without changes
|
{instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_directories.py
RENAMED
|
File without changes
|
{instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_entries.py
RENAMED
|
File without changes
|
{instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_reads.py
RENAMED
|
File without changes
|
{instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_upload.py
RENAMED
|
File without changes
|
{instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/user_data_writes.py
RENAMED
|
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.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{instance_repo-1.6.0.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|