asterstore 0.1.0__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.
- asterstore/__init__.py +84 -0
- asterstore/errors.py +75 -0
- asterstore/governance/__init__.py +13 -0
- asterstore/governance/_abandonment.py +60 -0
- asterstore/governance/_collection.py +178 -0
- asterstore/governance/_inventory.py +389 -0
- asterstore/governance/_references.py +163 -0
- asterstore/governance/_service.py +109 -0
- asterstore/governance/cleanup/__init__.py +5 -0
- asterstore/governance/cleanup/_service.py +224 -0
- asterstore/integrations/__init__.py +3 -0
- asterstore/integrations/polars/__init__.py +5 -0
- asterstore/integrations/polars/_parquet.py +34 -0
- asterstore/metadata/__init__.py +18 -0
- asterstore/metadata/capabilities/__init__.py +5 -0
- asterstore/metadata/capabilities/_models.py +88 -0
- asterstore/metadata/declarations/__init__.py +5 -0
- asterstore/metadata/declarations/_models.py +33 -0
- asterstore/metadata/identity/__init__.py +5 -0
- asterstore/metadata/identity/_validation.py +43 -0
- asterstore/metadata/membership/__init__.py +5 -0
- asterstore/metadata/membership/_models.py +90 -0
- asterstore/metadata/protocol/__init__.py +85 -0
- asterstore/metadata/protocol/_cleanup.py +138 -0
- asterstore/metadata/protocol/_codec.py +219 -0
- asterstore/metadata/protocol/_governance.py +286 -0
- asterstore/metadata/protocol/_json.py +66 -0
- asterstore/metadata/protocol/_models.py +154 -0
- asterstore/metadata/schemas/store.json +786 -0
- asterstore/publishing/__init__.py +6 -0
- asterstore/publishing/_candidate.py +321 -0
- asterstore/publishing/_membership.py +77 -0
- asterstore/publishing/_status.py +91 -0
- asterstore/publishing/transactions/__init__.py +5 -0
- asterstore/publishing/transactions/_commit.py +219 -0
- asterstore/py.typed +0 -0
- asterstore/reading/__init__.py +7 -0
- asterstore/reading/_binding.py +63 -0
- asterstore/reading/_catalog.py +31 -0
- asterstore/reading/_loading.py +72 -0
- asterstore/reading/_selection.py +18 -0
- asterstore/reading/resources/__init__.py +5 -0
- asterstore/reading/resources/_mapping.py +36 -0
- asterstore/registration/__init__.py +7 -0
- asterstore/registration/_operations.py +60 -0
- asterstore/repository.py +129 -0
- asterstore/storage/__init__.py +25 -0
- asterstore/storage/_deletion.py +46 -0
- asterstore/storage/_files.py +96 -0
- asterstore/storage/_layout.py +8 -0
- asterstore/storage/_locks.py +24 -0
- asterstore/storage/registry/__init__.py +62 -0
- asterstore/storage/registry/_cleanup.py +44 -0
- asterstore/storage/registry/_governance.py +122 -0
- asterstore/storage/registry/_initialize.py +48 -0
- asterstore/storage/registry/_managed.py +37 -0
- asterstore/storage/registry/_records.py +121 -0
- asterstore-0.1.0.dist-info/METADATA +144 -0
- asterstore-0.1.0.dist-info/RECORD +61 -0
- asterstore-0.1.0.dist-info/WHEEL +4 -0
- asterstore-0.1.0.dist-info/licenses/LICENSE +202 -0
asterstore/__init__.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Local data declarations, explicit publication, and lightweight bindings."""
|
|
2
|
+
|
|
3
|
+
from .errors import (
|
|
4
|
+
AsterStoreError,
|
|
5
|
+
CandidateAbandonedError,
|
|
6
|
+
CandidateStateError,
|
|
7
|
+
HistoryUnavailableError,
|
|
8
|
+
InvalidDeclarationError,
|
|
9
|
+
PublicationConflictError,
|
|
10
|
+
PublicationNotFoundError,
|
|
11
|
+
PublicationRetiredError,
|
|
12
|
+
ReferenceConflictError,
|
|
13
|
+
ReferenceNotFoundError,
|
|
14
|
+
RepositoryNotInitializedError,
|
|
15
|
+
ResourceNotBoundError,
|
|
16
|
+
StoreCorruptionError,
|
|
17
|
+
UnknownMemberError,
|
|
18
|
+
UnsupportedCapabilityError,
|
|
19
|
+
UnsupportedPlatformError,
|
|
20
|
+
)
|
|
21
|
+
from .governance import (
|
|
22
|
+
CleanupPreview,
|
|
23
|
+
CleanupResult,
|
|
24
|
+
Governance,
|
|
25
|
+
GovernancePreview,
|
|
26
|
+
GovernanceResult,
|
|
27
|
+
)
|
|
28
|
+
from .metadata.capabilities import (
|
|
29
|
+
ByteStability,
|
|
30
|
+
Capabilities,
|
|
31
|
+
HistoryAccess,
|
|
32
|
+
Management,
|
|
33
|
+
RetentionScope,
|
|
34
|
+
)
|
|
35
|
+
from .metadata.declarations import Declaration
|
|
36
|
+
from .metadata.membership import FileSet, Locator, Member, Object
|
|
37
|
+
from .metadata.protocol import DeclarationRecord, FixedRetention, ObjectRecord, StoreRecord
|
|
38
|
+
from .publishing import Candidate, CandidateStatus
|
|
39
|
+
from .reading import Binding
|
|
40
|
+
from .registration import RegistrationStatus
|
|
41
|
+
from .repository import Repository
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"CleanupPreview",
|
|
45
|
+
"CleanupResult",
|
|
46
|
+
"FixedRetention",
|
|
47
|
+
"Governance",
|
|
48
|
+
"GovernancePreview",
|
|
49
|
+
"GovernanceResult",
|
|
50
|
+
"Candidate",
|
|
51
|
+
"CandidateStatus",
|
|
52
|
+
"DeclarationRecord",
|
|
53
|
+
"ObjectRecord",
|
|
54
|
+
"StoreRecord",
|
|
55
|
+
"RegistrationStatus",
|
|
56
|
+
"ResourceNotBoundError",
|
|
57
|
+
"UnknownMemberError",
|
|
58
|
+
"UnsupportedCapabilityError",
|
|
59
|
+
"ByteStability",
|
|
60
|
+
"Capabilities",
|
|
61
|
+
"HistoryAccess",
|
|
62
|
+
"Management",
|
|
63
|
+
"RetentionScope",
|
|
64
|
+
"Declaration",
|
|
65
|
+
"FileSet",
|
|
66
|
+
"Locator",
|
|
67
|
+
"Member",
|
|
68
|
+
"Object",
|
|
69
|
+
"CandidateAbandonedError",
|
|
70
|
+
"PublicationRetiredError",
|
|
71
|
+
"ReferenceConflictError",
|
|
72
|
+
"ReferenceNotFoundError",
|
|
73
|
+
"AsterStoreError",
|
|
74
|
+
"Binding",
|
|
75
|
+
"CandidateStateError",
|
|
76
|
+
"InvalidDeclarationError",
|
|
77
|
+
"HistoryUnavailableError",
|
|
78
|
+
"PublicationConflictError",
|
|
79
|
+
"PublicationNotFoundError",
|
|
80
|
+
"Repository",
|
|
81
|
+
"RepositoryNotInitializedError",
|
|
82
|
+
"StoreCorruptionError",
|
|
83
|
+
"UnsupportedPlatformError",
|
|
84
|
+
]
|
asterstore/errors.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Exceptions exposed by the implemented declaration and binding API."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AsterStoreError(Exception):
|
|
5
|
+
"""Base class for asterstore errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InvalidDeclarationError(AsterStoreError, ValueError):
|
|
9
|
+
"""A supplied declaration is structurally invalid."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StoreCorruptionError(AsterStoreError):
|
|
13
|
+
"""Persistent metadata is invalid or inconsistent."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RepositoryNotInitializedError(AsterStoreError, FileNotFoundError):
|
|
17
|
+
"""The root does not contain an initialized asterstore repository."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PublicationNotFoundError(AsterStoreError, FileNotFoundError):
|
|
21
|
+
"""The requested committed publication does not exist."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PublicationConflictError(AsterStoreError):
|
|
25
|
+
"""The publication's base generation or identity conflicts with stored state."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CandidateStateError(AsterStoreError):
|
|
29
|
+
"""An operation is not valid for the candidate's current state."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class HistoryUnavailableError(AsterStoreError):
|
|
33
|
+
"""A dataset does not promise historical content access."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class UnsupportedPlatformError(AsterStoreError):
|
|
37
|
+
"""The required local filesystem coordination is unavailable."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ReferenceConflictError(AsterStoreError):
|
|
41
|
+
"""The named reference revision or create request does not match."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ReferenceNotFoundError(AsterStoreError, KeyError):
|
|
45
|
+
"""The reference name has never been recorded."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PublicationRetiredError(AsterStoreError):
|
|
49
|
+
"""An identity was committed and irreversibly retired; it cannot be reopened."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, dataset_id: str, publication_id: str, operation_id: str) -> None:
|
|
52
|
+
self.dataset_id = dataset_id
|
|
53
|
+
self.publication_id = publication_id
|
|
54
|
+
self.operation_id = operation_id
|
|
55
|
+
super().__init__(f"publication is retired: {dataset_id}/{publication_id} ({operation_id})")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CandidateAbandonedError(CandidateStateError):
|
|
59
|
+
"""The candidate was permanently abandoned and cannot resume or commit."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, operation_id: str) -> None:
|
|
62
|
+
self.operation_id = operation_id
|
|
63
|
+
super().__init__(f"candidate is abandoned: {operation_id}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class UnsupportedCapabilityError(AsterStoreError, ValueError):
|
|
67
|
+
"""The declared profile cannot support the requested governance guarantee."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ResourceNotBoundError(AsterStoreError, KeyError):
|
|
71
|
+
"""A declaration refers to a resource without an explicit root binding."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class UnknownMemberError(AsterStoreError, KeyError):
|
|
75
|
+
"""A requested logical member is absent from the bound declaration."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Explicit retention, retirement and recoverable collection."""
|
|
2
|
+
|
|
3
|
+
from ._collection import GovernancePreview, GovernanceResult
|
|
4
|
+
from ._service import Governance
|
|
5
|
+
from .cleanup import CleanupPreview, CleanupResult
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"CleanupPreview",
|
|
9
|
+
"CleanupResult",
|
|
10
|
+
"Governance",
|
|
11
|
+
"GovernancePreview",
|
|
12
|
+
"GovernanceResult",
|
|
13
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Persist the control evidence needed to interpret irreversible abandonment."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from asterstore.errors import StoreCorruptionError
|
|
6
|
+
from asterstore.metadata.membership import Object
|
|
7
|
+
from asterstore.metadata.protocol import (
|
|
8
|
+
DeclarationRecord,
|
|
9
|
+
ManagedRequest,
|
|
10
|
+
StoreRecord,
|
|
11
|
+
decode_declaration_record,
|
|
12
|
+
)
|
|
13
|
+
from asterstore.storage import sync_control_files
|
|
14
|
+
from asterstore.storage.registry import (
|
|
15
|
+
managed_directory,
|
|
16
|
+
object_record_path,
|
|
17
|
+
operation_path,
|
|
18
|
+
read_object_record,
|
|
19
|
+
read_operation,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from ._inventory import checked, control_bytes, entries
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def sync_candidate_evidence(root: Path, store: StoreRecord, request: ManagedRequest) -> None:
|
|
26
|
+
"""Caller holds writer and root locks; missing partial-commit records are allowed.
|
|
27
|
+
|
|
28
|
+
This deliberately does not require full inventory: abandonment must remain
|
|
29
|
+
available after interruption before the initial protection write succeeds.
|
|
30
|
+
Existing candidate/creator evidence is checked and synced; data bytes are not.
|
|
31
|
+
"""
|
|
32
|
+
directory = managed_directory(root, request.operation_id)
|
|
33
|
+
children = set(entries(directory))
|
|
34
|
+
paths = {root / ".asterstore/format.json", directory / "request.json"}
|
|
35
|
+
records: dict[str, DeclarationRecord] = {}
|
|
36
|
+
objects: dict[str, Object] = {}
|
|
37
|
+
for name in ("sealed.json", "protection.json"):
|
|
38
|
+
path = directory / name
|
|
39
|
+
if path not in children:
|
|
40
|
+
continue
|
|
41
|
+
record = checked(decode_declaration_record(control_bytes(path, paths)), store)
|
|
42
|
+
if record != request.declaration_record(record.declaration.files):
|
|
43
|
+
raise StoreCorruptionError("candidate evidence differs from abandonment request")
|
|
44
|
+
records[name] = record
|
|
45
|
+
for obj in record.declaration.files.objects:
|
|
46
|
+
if obj.object_id in objects and objects[obj.object_id] != obj:
|
|
47
|
+
raise StoreCorruptionError("candidate evidence has conflicting objects")
|
|
48
|
+
objects[obj.object_id] = obj
|
|
49
|
+
operation = read_operation(root, store, request.operation_id)
|
|
50
|
+
if operation is not None:
|
|
51
|
+
if operation != records.get("sealed.json"):
|
|
52
|
+
raise StoreCorruptionError("managed operation lacks matching sealed evidence")
|
|
53
|
+
paths.add(operation_path(root, request.operation_id))
|
|
54
|
+
for obj in objects.values():
|
|
55
|
+
proof = read_object_record(root, store, obj.object_id)
|
|
56
|
+
if proof is not None:
|
|
57
|
+
if proof.locator != obj.locator or proof.creator_operation_id is None:
|
|
58
|
+
raise StoreCorruptionError("candidate object proof conflicts with its declaration")
|
|
59
|
+
paths.add(object_record_path(root, obj.object_id))
|
|
60
|
+
sync_control_files(root, tuple(paths))
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Coordinated, fixed-plan collection. Only proven owned object files may be deleted."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, replace
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from asterstore.errors import PublicationNotFoundError, StoreCorruptionError
|
|
8
|
+
from asterstore.metadata.capabilities import Management
|
|
9
|
+
from asterstore.metadata.protocol import (
|
|
10
|
+
DeclarationRecord,
|
|
11
|
+
GovernancePlan,
|
|
12
|
+
GovernanceProgress,
|
|
13
|
+
ObjectRecord,
|
|
14
|
+
RetiredDeclaration,
|
|
15
|
+
encode_governance_plan,
|
|
16
|
+
encode_governance_progress,
|
|
17
|
+
encode_retired,
|
|
18
|
+
)
|
|
19
|
+
from asterstore.storage import (
|
|
20
|
+
atomic_write,
|
|
21
|
+
delete_owned_file,
|
|
22
|
+
file_lock,
|
|
23
|
+
immutable_write,
|
|
24
|
+
sync_control_files,
|
|
25
|
+
)
|
|
26
|
+
from asterstore.storage.registry import (
|
|
27
|
+
collection_directory,
|
|
28
|
+
lifecycle_store,
|
|
29
|
+
managed_data_root,
|
|
30
|
+
read_governance_plan,
|
|
31
|
+
read_governance_progress,
|
|
32
|
+
retired_path,
|
|
33
|
+
token,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from ._inventory import Inventory, PublicationKey, inventory, key
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class GovernancePreview:
|
|
41
|
+
retiring: tuple[DeclarationRecord, ...]
|
|
42
|
+
reclaimable: tuple[ObjectRecord, ...]
|
|
43
|
+
protected_objects: tuple[str, ...]
|
|
44
|
+
retained_metadata: tuple[PublicationKey, ...]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class GovernanceResult:
|
|
49
|
+
operation_id: str
|
|
50
|
+
retired_publications: tuple[PublicationKey, ...]
|
|
51
|
+
deleted_objects: tuple[str, ...]
|
|
52
|
+
missing_objects: tuple[str, ...]
|
|
53
|
+
protected_objects: tuple[str, ...]
|
|
54
|
+
complete: bool
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def preview_inventory(view: Inventory) -> GovernancePreview:
|
|
58
|
+
retiring = tuple(
|
|
59
|
+
value
|
|
60
|
+
for identity, value in sorted(view.publications.items())
|
|
61
|
+
if identity not in view.protected_publications
|
|
62
|
+
and identity not in view.retired
|
|
63
|
+
and value.declaration.capabilities.management is Management.MANAGED
|
|
64
|
+
)
|
|
65
|
+
reclaimable = tuple(
|
|
66
|
+
value
|
|
67
|
+
for oid, value in sorted(view.objects.items())
|
|
68
|
+
if value.creator_operation_id is not None
|
|
69
|
+
and oid not in view.protected_objects
|
|
70
|
+
and oid not in view.collected_objects
|
|
71
|
+
and oid not in view.cleaned_objects
|
|
72
|
+
)
|
|
73
|
+
return GovernancePreview(
|
|
74
|
+
retiring,
|
|
75
|
+
reclaimable,
|
|
76
|
+
tuple(sorted(view.protected_objects)),
|
|
77
|
+
tuple(sorted(view.retained_metadata)),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def preview(root: Path) -> GovernancePreview:
|
|
82
|
+
lifecycle_store(root)
|
|
83
|
+
with file_lock(root / ".asterstore/gc.lock", exclusive=False):
|
|
84
|
+
return preview_inventory(inventory(root, lifecycle_store(root)))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def result(value: GovernanceProgress) -> GovernanceResult:
|
|
88
|
+
return GovernanceResult(
|
|
89
|
+
value.operation_id,
|
|
90
|
+
value.retired,
|
|
91
|
+
tuple(oid for oid, state in value.outcomes if state == "deleted"),
|
|
92
|
+
tuple(oid for oid, state in value.outcomes if state == "missing"),
|
|
93
|
+
tuple(oid for oid, state in value.outcomes if state == "protected"),
|
|
94
|
+
value.complete,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def execute(root: Path, plan: GovernancePlan, view: Inventory) -> GovernanceResult:
|
|
99
|
+
progress = read_governance_progress(root, plan)
|
|
100
|
+
# Persist deletion evidence, including weak publications and candidate roots.
|
|
101
|
+
sync_control_files(
|
|
102
|
+
root,
|
|
103
|
+
tuple(view.control_paths | {collection_directory(root, plan.operation_id) / "plan.json"}),
|
|
104
|
+
)
|
|
105
|
+
if progress.complete:
|
|
106
|
+
return result(progress)
|
|
107
|
+
retired = set(progress.retired)
|
|
108
|
+
for publication in plan.retirements:
|
|
109
|
+
identity = key(publication)
|
|
110
|
+
if view.publications.get(identity) != publication:
|
|
111
|
+
raise StoreCorruptionError("planned retirement no longer matches authority")
|
|
112
|
+
if identity in view.protected_publications:
|
|
113
|
+
continue
|
|
114
|
+
if identity not in view.retired:
|
|
115
|
+
value = RetiredDeclaration(plan.operation_id, publication)
|
|
116
|
+
immutable_write(retired_path(root, *identity), encode_retired(value), durable=True)
|
|
117
|
+
view.retired[identity] = value
|
|
118
|
+
retired.add(identity)
|
|
119
|
+
progress = replace(progress, retired=tuple(sorted(retired)))
|
|
120
|
+
outcomes = dict(progress.outcomes)
|
|
121
|
+
for obj in plan.objects:
|
|
122
|
+
if obj.object_id in outcomes:
|
|
123
|
+
continue
|
|
124
|
+
if view.objects.get(obj.object_id) != obj:
|
|
125
|
+
raise StoreCorruptionError("planned object no longer matches creator evidence")
|
|
126
|
+
state: Literal["deleted", "missing", "protected"]
|
|
127
|
+
if obj.object_id in view.protected_objects:
|
|
128
|
+
state = "protected"
|
|
129
|
+
else:
|
|
130
|
+
# No live published declaration may lose its bytes before retirement is durable.
|
|
131
|
+
owners = view.object_users.get(obj.object_id, set())
|
|
132
|
+
if not owners or any(identity not in view.retired for identity in owners):
|
|
133
|
+
raise StoreCorruptionError("object lacks complete durable retirement evidence")
|
|
134
|
+
state = (
|
|
135
|
+
"deleted"
|
|
136
|
+
if delete_owned_file(managed_data_root(root), obj.locator.relative_path)
|
|
137
|
+
else "missing"
|
|
138
|
+
)
|
|
139
|
+
outcomes[obj.object_id] = state
|
|
140
|
+
# Geometric checkpoints keep cumulative progress serialization linear in
|
|
141
|
+
# plan size. A crash between checkpoints is recovered as missing files.
|
|
142
|
+
count = len(outcomes)
|
|
143
|
+
if count & (count - 1) == 0:
|
|
144
|
+
progress = replace(progress, outcomes=tuple(outcomes.items()))
|
|
145
|
+
atomic_write(
|
|
146
|
+
collection_directory(root, plan.operation_id) / "progress.json",
|
|
147
|
+
encode_governance_progress(progress),
|
|
148
|
+
durable=True,
|
|
149
|
+
)
|
|
150
|
+
progress = replace(progress, outcomes=tuple(outcomes.items()), complete=True)
|
|
151
|
+
atomic_write(
|
|
152
|
+
collection_directory(root, plan.operation_id) / "progress.json",
|
|
153
|
+
encode_governance_progress(progress),
|
|
154
|
+
durable=True,
|
|
155
|
+
)
|
|
156
|
+
return result(progress)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def collect(root: Path, operation_id: str, *, resume: bool = False) -> GovernanceResult:
|
|
160
|
+
token(operation_id)
|
|
161
|
+
lifecycle_store(root)
|
|
162
|
+
with file_lock(root / ".asterstore/gc.lock", exclusive=True):
|
|
163
|
+
store = lifecycle_store(root)
|
|
164
|
+
plan = read_governance_plan(root, store, operation_id)
|
|
165
|
+
if plan is None and resume:
|
|
166
|
+
raise PublicationNotFoundError("collection operation does not exist")
|
|
167
|
+
view = inventory(root, store)
|
|
168
|
+
if plan is None:
|
|
169
|
+
selection = preview_inventory(view)
|
|
170
|
+
plan = GovernancePlan(
|
|
171
|
+
store.store_id, operation_id, selection.retiring, selection.reclaimable
|
|
172
|
+
)
|
|
173
|
+
immutable_write(
|
|
174
|
+
collection_directory(root, operation_id) / "plan.json",
|
|
175
|
+
encode_governance_plan(plan),
|
|
176
|
+
durable=True,
|
|
177
|
+
)
|
|
178
|
+
return execute(root, plan, view)
|