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.
Files changed (61) hide show
  1. asterstore/__init__.py +84 -0
  2. asterstore/errors.py +75 -0
  3. asterstore/governance/__init__.py +13 -0
  4. asterstore/governance/_abandonment.py +60 -0
  5. asterstore/governance/_collection.py +178 -0
  6. asterstore/governance/_inventory.py +389 -0
  7. asterstore/governance/_references.py +163 -0
  8. asterstore/governance/_service.py +109 -0
  9. asterstore/governance/cleanup/__init__.py +5 -0
  10. asterstore/governance/cleanup/_service.py +224 -0
  11. asterstore/integrations/__init__.py +3 -0
  12. asterstore/integrations/polars/__init__.py +5 -0
  13. asterstore/integrations/polars/_parquet.py +34 -0
  14. asterstore/metadata/__init__.py +18 -0
  15. asterstore/metadata/capabilities/__init__.py +5 -0
  16. asterstore/metadata/capabilities/_models.py +88 -0
  17. asterstore/metadata/declarations/__init__.py +5 -0
  18. asterstore/metadata/declarations/_models.py +33 -0
  19. asterstore/metadata/identity/__init__.py +5 -0
  20. asterstore/metadata/identity/_validation.py +43 -0
  21. asterstore/metadata/membership/__init__.py +5 -0
  22. asterstore/metadata/membership/_models.py +90 -0
  23. asterstore/metadata/protocol/__init__.py +85 -0
  24. asterstore/metadata/protocol/_cleanup.py +138 -0
  25. asterstore/metadata/protocol/_codec.py +219 -0
  26. asterstore/metadata/protocol/_governance.py +286 -0
  27. asterstore/metadata/protocol/_json.py +66 -0
  28. asterstore/metadata/protocol/_models.py +154 -0
  29. asterstore/metadata/schemas/store.json +786 -0
  30. asterstore/publishing/__init__.py +6 -0
  31. asterstore/publishing/_candidate.py +321 -0
  32. asterstore/publishing/_membership.py +77 -0
  33. asterstore/publishing/_status.py +91 -0
  34. asterstore/publishing/transactions/__init__.py +5 -0
  35. asterstore/publishing/transactions/_commit.py +219 -0
  36. asterstore/py.typed +0 -0
  37. asterstore/reading/__init__.py +7 -0
  38. asterstore/reading/_binding.py +63 -0
  39. asterstore/reading/_catalog.py +31 -0
  40. asterstore/reading/_loading.py +72 -0
  41. asterstore/reading/_selection.py +18 -0
  42. asterstore/reading/resources/__init__.py +5 -0
  43. asterstore/reading/resources/_mapping.py +36 -0
  44. asterstore/registration/__init__.py +7 -0
  45. asterstore/registration/_operations.py +60 -0
  46. asterstore/repository.py +129 -0
  47. asterstore/storage/__init__.py +25 -0
  48. asterstore/storage/_deletion.py +46 -0
  49. asterstore/storage/_files.py +96 -0
  50. asterstore/storage/_layout.py +8 -0
  51. asterstore/storage/_locks.py +24 -0
  52. asterstore/storage/registry/__init__.py +62 -0
  53. asterstore/storage/registry/_cleanup.py +44 -0
  54. asterstore/storage/registry/_governance.py +122 -0
  55. asterstore/storage/registry/_initialize.py +48 -0
  56. asterstore/storage/registry/_managed.py +37 -0
  57. asterstore/storage/registry/_records.py +121 -0
  58. asterstore-0.1.0.dist-info/METADATA +144 -0
  59. asterstore-0.1.0.dist-info/RECORD +61 -0
  60. asterstore-0.1.0.dist-info/WHEEL +4 -0
  61. 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)