massive-workflows 0.1.0__py3-none-win_amd64.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.
- massive/__init__.py +50 -0
- massive/_bin/massive.exe +0 -0
- massive/_step.py +56 -0
- massive/artifact.py +356 -0
- massive/builder.py +1278 -0
- massive/canonical.py +69 -0
- massive/cli.py +28 -0
- massive/context.py +31 -0
- massive/contracts.py +172 -0
- massive/datastore.py +268 -0
- massive/files.py +276 -0
- massive/frontend.py +178 -0
- massive/hashing.py +71 -0
- massive/identity.py +79 -0
- massive/py.typed +0 -0
- massive/runner.py +486 -0
- massive/schemas/data-artifact-manifest.schema.json +84 -0
- massive/schemas/step-invocation-descriptor.schema.json +207 -0
- massive/schemas/workflow-spec.schema.json +412 -0
- massive/source_package.py +86 -0
- massive_workflows-0.1.0.dist-info/METADATA +655 -0
- massive_workflows-0.1.0.dist-info/RECORD +25 -0
- massive_workflows-0.1.0.dist-info/WHEEL +4 -0
- massive_workflows-0.1.0.dist-info/entry_points.txt +4 -0
- massive_workflows-0.1.0.dist-info/licenses/LICENSE +21 -0
massive/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Typed, portable workflow authoring for Massive."""
|
|
2
|
+
|
|
3
|
+
from .builder import (
|
|
4
|
+
DEFAULT_MAP_CONCURRENCY,
|
|
5
|
+
MAX_MAP_CONCURRENCY,
|
|
6
|
+
CaseHandle,
|
|
7
|
+
DecisionHandle,
|
|
8
|
+
GraphBuilder,
|
|
9
|
+
NodeHandle,
|
|
10
|
+
WorkflowSpec,
|
|
11
|
+
)
|
|
12
|
+
from .canonical import JsonValue, canonical_json, sha256_ref
|
|
13
|
+
from .context import InvocationContext, NonRetryableError, StepContext
|
|
14
|
+
from .contracts import (
|
|
15
|
+
Container,
|
|
16
|
+
ExecutionContract,
|
|
17
|
+
Retry,
|
|
18
|
+
container,
|
|
19
|
+
execution,
|
|
20
|
+
retry,
|
|
21
|
+
)
|
|
22
|
+
from .files import ArtifactFiles, Blob, Tree
|
|
23
|
+
from .source_package import SourcePackage, source_package
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"DEFAULT_MAP_CONCURRENCY",
|
|
27
|
+
"MAX_MAP_CONCURRENCY",
|
|
28
|
+
"ArtifactFiles",
|
|
29
|
+
"Blob",
|
|
30
|
+
"CaseHandle",
|
|
31
|
+
"Container",
|
|
32
|
+
"DecisionHandle",
|
|
33
|
+
"ExecutionContract",
|
|
34
|
+
"GraphBuilder",
|
|
35
|
+
"InvocationContext",
|
|
36
|
+
"JsonValue",
|
|
37
|
+
"NodeHandle",
|
|
38
|
+
"NonRetryableError",
|
|
39
|
+
"Retry",
|
|
40
|
+
"SourcePackage",
|
|
41
|
+
"StepContext",
|
|
42
|
+
"Tree",
|
|
43
|
+
"WorkflowSpec",
|
|
44
|
+
"canonical_json",
|
|
45
|
+
"container",
|
|
46
|
+
"execution",
|
|
47
|
+
"retry",
|
|
48
|
+
"sha256_ref",
|
|
49
|
+
"source_package",
|
|
50
|
+
]
|
massive/_bin/massive.exe
ADDED
|
Binary file
|
massive/_step.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Generic, TypeVar, get_args, get_origin, get_type_hints
|
|
7
|
+
|
|
8
|
+
from .context import InputT, StepContext
|
|
9
|
+
from .contracts import ExecutionContract
|
|
10
|
+
|
|
11
|
+
OutputT = TypeVar("OutputT")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class StepDefinition(Generic[InputT, OutputT]):
|
|
16
|
+
function: Callable[[StepContext[InputT]], OutputT | Awaitable[OutputT]]
|
|
17
|
+
input_type: Any
|
|
18
|
+
output_type: Any
|
|
19
|
+
contract: ExecutionContract | None
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_callable(
|
|
23
|
+
cls,
|
|
24
|
+
function: Callable[[StepContext[InputT]], OutputT | Awaitable[OutputT]],
|
|
25
|
+
*,
|
|
26
|
+
contract: ExecutionContract | None = None,
|
|
27
|
+
) -> StepDefinition[InputT, OutputT]:
|
|
28
|
+
if (
|
|
29
|
+
not inspect.isfunction(function)
|
|
30
|
+
or function.__qualname__ != function.__name__
|
|
31
|
+
or function.__name__ == "<lambda>"
|
|
32
|
+
):
|
|
33
|
+
raise TypeError("workflow steps must be top-level named functions")
|
|
34
|
+
hints = get_type_hints(function, include_extras=True)
|
|
35
|
+
parameters = list(inspect.signature(function).parameters.values())
|
|
36
|
+
if (
|
|
37
|
+
len(parameters) != 1
|
|
38
|
+
or parameters[0].name not in hints
|
|
39
|
+
or parameters[0].kind
|
|
40
|
+
not in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
41
|
+
):
|
|
42
|
+
raise TypeError(
|
|
43
|
+
"a workflow step requires one annotated positional StepContext parameter"
|
|
44
|
+
)
|
|
45
|
+
context_type = hints[parameters[0].name]
|
|
46
|
+
if get_origin(context_type) is not StepContext:
|
|
47
|
+
raise TypeError("a workflow step parameter must be StepContext[Input]")
|
|
48
|
+
(input_type,) = get_args(context_type)
|
|
49
|
+
if hints.get("return", inspect.Signature.empty) is inspect.Signature.empty:
|
|
50
|
+
raise TypeError("a workflow step requires a return annotation")
|
|
51
|
+
return cls(
|
|
52
|
+
function=function,
|
|
53
|
+
input_type=input_type,
|
|
54
|
+
output_type=hints["return"],
|
|
55
|
+
contract=contract,
|
|
56
|
+
)
|
massive/artifact.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.resources
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Protocol, cast
|
|
10
|
+
|
|
11
|
+
from jsonschema import Draft202012Validator
|
|
12
|
+
from jsonschema.exceptions import SchemaError, ValidationError
|
|
13
|
+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, StrictStr
|
|
14
|
+
from referencing.exceptions import Unresolvable
|
|
15
|
+
|
|
16
|
+
from .canonical import (
|
|
17
|
+
CanonicalJsonError,
|
|
18
|
+
JsonValue,
|
|
19
|
+
canonical_json,
|
|
20
|
+
parse_canonical_json,
|
|
21
|
+
sha256_ref,
|
|
22
|
+
)
|
|
23
|
+
from .datastore import Datastore, DatastoreConflictError, DatastoreNotFoundError
|
|
24
|
+
from .identity import (
|
|
25
|
+
ExecutionScope,
|
|
26
|
+
PositiveAttempt,
|
|
27
|
+
ProjectKey,
|
|
28
|
+
SafePathSegment,
|
|
29
|
+
Sha256Reference,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
JSON_CONTENT_TYPE = "application/json"
|
|
33
|
+
MANIFEST_CONTENT_TYPE = "application/vnd.massive.data-artifact-manifest+json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ArtifactError(Exception):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ArtifactValidationError(ArtifactError):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ArtifactIntegrityError(ArtifactError):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ArtifactNotFoundError(ArtifactError):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ArtifactBodyConflictError(ArtifactError):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ArtifactManifestConflictError(ArtifactError):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SchemaValidator(Protocol):
|
|
61
|
+
def validate(self, value: object) -> None: ...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Destination(BaseModel):
|
|
65
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
66
|
+
|
|
67
|
+
manifest_key: StrictStr = Field(
|
|
68
|
+
validation_alias=AliasChoices("manifest_key", "manifestKey"),
|
|
69
|
+
serialization_alias="manifestKey",
|
|
70
|
+
min_length=1,
|
|
71
|
+
)
|
|
72
|
+
schema_ref: Sha256Reference = Field(
|
|
73
|
+
validation_alias=AliasChoices("schema_ref", "schema"), serialization_alias="schema"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Producer(BaseModel):
|
|
78
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
79
|
+
|
|
80
|
+
project_key: ProjectKey = Field(
|
|
81
|
+
validation_alias=AliasChoices("project_key", "projectKey"), serialization_alias="projectKey"
|
|
82
|
+
)
|
|
83
|
+
plan_hash: Sha256Reference = Field(
|
|
84
|
+
validation_alias=AliasChoices("plan_hash", "planHash"), serialization_alias="planHash"
|
|
85
|
+
)
|
|
86
|
+
run_id: SafePathSegment = Field(
|
|
87
|
+
validation_alias=AliasChoices("run_id", "runId"), serialization_alias="runId"
|
|
88
|
+
)
|
|
89
|
+
node_id: SafePathSegment = Field(
|
|
90
|
+
validation_alias=AliasChoices("node_id", "nodeId"), serialization_alias="nodeId"
|
|
91
|
+
)
|
|
92
|
+
attempt: PositiveAttempt
|
|
93
|
+
scope: ExecutionScope | None = None
|
|
94
|
+
|
|
95
|
+
def identity_json(self) -> dict[str, JsonValue]:
|
|
96
|
+
return cast(
|
|
97
|
+
dict[str, JsonValue], self.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True, slots=True)
|
|
102
|
+
class ArtifactRef:
|
|
103
|
+
key: str
|
|
104
|
+
hash: str
|
|
105
|
+
size: int
|
|
106
|
+
content_type: str
|
|
107
|
+
|
|
108
|
+
def json(self) -> dict[str, JsonValue]:
|
|
109
|
+
return {
|
|
110
|
+
"key": self.key,
|
|
111
|
+
"hash": self.hash,
|
|
112
|
+
"size": self.size,
|
|
113
|
+
"contentType": self.content_type,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True, slots=True)
|
|
118
|
+
class PublishedJSON:
|
|
119
|
+
manifest: ArtifactRef
|
|
120
|
+
body: ArtifactRef
|
|
121
|
+
schema: str
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ArtifactRuntime:
|
|
125
|
+
"""Publishes JSON through an immutable body followed by a manifest commit point."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, datastore: Datastore) -> None:
|
|
128
|
+
self._datastore = datastore
|
|
129
|
+
|
|
130
|
+
def validate_destination(self, destination: Destination, producer: Producer) -> None:
|
|
131
|
+
"""Validate an immutable producer slot before invoking user code."""
|
|
132
|
+
_validate_destination(destination, producer)
|
|
133
|
+
|
|
134
|
+
def publish_json(
|
|
135
|
+
self, destination: Destination, producer: Producer, body: bytes
|
|
136
|
+
) -> PublishedJSON:
|
|
137
|
+
_validate_destination(destination, producer)
|
|
138
|
+
_validate_canonical_json(self._datastore, destination.schema_ref, body)
|
|
139
|
+
body_hash = sha256_ref(body)
|
|
140
|
+
body_ref = ArtifactRef(
|
|
141
|
+
key=blob_key(body_hash),
|
|
142
|
+
hash=body_hash,
|
|
143
|
+
size=len(body),
|
|
144
|
+
content_type=JSON_CONTENT_TYPE,
|
|
145
|
+
)
|
|
146
|
+
manifest = cast(
|
|
147
|
+
dict[str, JsonValue],
|
|
148
|
+
{
|
|
149
|
+
"kind": "DataArtifactManifest",
|
|
150
|
+
"schemaVersion": 1,
|
|
151
|
+
"encoding": "canonical-json-v0",
|
|
152
|
+
"producer": producer.identity_json(),
|
|
153
|
+
"schema": destination.schema_ref,
|
|
154
|
+
"body": body_ref.json(),
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
manifest_body = _canonical_manifest(manifest)
|
|
158
|
+
put_immutable(
|
|
159
|
+
self._datastore, body_ref.key, body, JSON_CONTENT_TYPE, ArtifactBodyConflictError
|
|
160
|
+
)
|
|
161
|
+
put_immutable(
|
|
162
|
+
self._datastore,
|
|
163
|
+
destination.manifest_key,
|
|
164
|
+
manifest_body,
|
|
165
|
+
MANIFEST_CONTENT_TYPE,
|
|
166
|
+
ArtifactManifestConflictError,
|
|
167
|
+
)
|
|
168
|
+
return PublishedJSON(
|
|
169
|
+
manifest=ArtifactRef(
|
|
170
|
+
key=destination.manifest_key,
|
|
171
|
+
hash=sha256_ref(manifest_body),
|
|
172
|
+
size=len(manifest_body),
|
|
173
|
+
content_type=MANIFEST_CONTENT_TYPE,
|
|
174
|
+
),
|
|
175
|
+
body=body_ref,
|
|
176
|
+
schema=destination.schema_ref,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def resolve_json(
|
|
180
|
+
self, destination: Destination, producer: Producer
|
|
181
|
+
) -> tuple[PublishedJSON, bytes]:
|
|
182
|
+
_validate_destination(destination, producer)
|
|
183
|
+
try:
|
|
184
|
+
manifest_object = self._datastore.get(destination.manifest_key)
|
|
185
|
+
except DatastoreNotFoundError as error:
|
|
186
|
+
raise ArtifactNotFoundError(
|
|
187
|
+
f"artifact manifest {destination.manifest_key} is missing"
|
|
188
|
+
) from error
|
|
189
|
+
if manifest_object.info.content_type != MANIFEST_CONTENT_TYPE:
|
|
190
|
+
raise ArtifactIntegrityError(
|
|
191
|
+
f"manifest {destination.manifest_key} has unexpected content type"
|
|
192
|
+
)
|
|
193
|
+
manifest = _parse_canonical_json(manifest_object.body, "manifest", ArtifactIntegrityError)
|
|
194
|
+
try:
|
|
195
|
+
_manifest_validator().validate(manifest)
|
|
196
|
+
except ValidationError as error:
|
|
197
|
+
raise ArtifactIntegrityError("manifest does not satisfy its schema") from error
|
|
198
|
+
if not isinstance(manifest, dict):
|
|
199
|
+
raise ArtifactIntegrityError("manifest must be an object")
|
|
200
|
+
expected_producer = producer.identity_json()
|
|
201
|
+
if (
|
|
202
|
+
manifest.get("producer") != expected_producer
|
|
203
|
+
or manifest.get("schema") != destination.schema_ref
|
|
204
|
+
):
|
|
205
|
+
raise ArtifactIntegrityError("manifest does not match its expected producer and schema")
|
|
206
|
+
body = manifest["body"]
|
|
207
|
+
if not isinstance(body, dict):
|
|
208
|
+
raise ArtifactIntegrityError("manifest body must be an object")
|
|
209
|
+
body_ref = ArtifactRef(
|
|
210
|
+
key=cast(str, body["key"]),
|
|
211
|
+
hash=cast(str, body["hash"]),
|
|
212
|
+
size=cast(int, body["size"]),
|
|
213
|
+
content_type=cast(str, body["contentType"]),
|
|
214
|
+
)
|
|
215
|
+
if body_ref.key != blob_key(body_ref.hash):
|
|
216
|
+
raise ArtifactIntegrityError("manifest body key does not match its digest")
|
|
217
|
+
try:
|
|
218
|
+
body_object = self._datastore.get(body_ref.key)
|
|
219
|
+
except DatastoreNotFoundError as error:
|
|
220
|
+
raise ArtifactIntegrityError(f"manifest body {body_ref.key} is missing") from error
|
|
221
|
+
if (
|
|
222
|
+
body_object.info.content_type != JSON_CONTENT_TYPE
|
|
223
|
+
or len(body_object.body) != body_ref.size
|
|
224
|
+
or sha256_ref(body_object.body) != body_ref.hash
|
|
225
|
+
):
|
|
226
|
+
raise ArtifactIntegrityError("manifest body does not match its reference")
|
|
227
|
+
try:
|
|
228
|
+
_validate_canonical_json(self._datastore, destination.schema_ref, body_object.body)
|
|
229
|
+
except ArtifactValidationError as error:
|
|
230
|
+
raise ArtifactIntegrityError("manifest body does not satisfy its schema") from error
|
|
231
|
+
return (
|
|
232
|
+
PublishedJSON(
|
|
233
|
+
manifest=ArtifactRef(
|
|
234
|
+
key=destination.manifest_key,
|
|
235
|
+
hash=sha256_ref(manifest_object.body),
|
|
236
|
+
size=len(manifest_object.body),
|
|
237
|
+
content_type=MANIFEST_CONTENT_TYPE,
|
|
238
|
+
),
|
|
239
|
+
body=body_ref,
|
|
240
|
+
schema=destination.schema_ref,
|
|
241
|
+
),
|
|
242
|
+
body_object.body,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _validate_destination(destination: Destination, producer: Producer) -> None:
|
|
247
|
+
expected_key = (
|
|
248
|
+
f"projects/{producer.project_key}/runs/{producer.run_id}/steps/"
|
|
249
|
+
f"{producer.node_id}{_scope_key_suffix(producer.scope)}/{producer.attempt}/output-manifest.json"
|
|
250
|
+
)
|
|
251
|
+
if destination.manifest_key != expected_key:
|
|
252
|
+
raise ArtifactValidationError(
|
|
253
|
+
f"manifest destination {destination.manifest_key!r} does not match producer slot"
|
|
254
|
+
)
|
|
255
|
+
try:
|
|
256
|
+
blob_key(destination.schema_ref)
|
|
257
|
+
except ArtifactValidationError as error:
|
|
258
|
+
raise ArtifactValidationError("schema reference must be a SHA-256 reference") from error
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _scope_key_suffix(scope: ExecutionScope | None) -> str:
|
|
262
|
+
if scope is None:
|
|
263
|
+
return ""
|
|
264
|
+
return "/scopes" + "".join(
|
|
265
|
+
f"/maps/{frame.map_id}/items/{frame.index}" for frame in scope.frames
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _validate_canonical_json(datastore: Datastore, schema_ref: str, body: bytes) -> None:
|
|
270
|
+
document = _parse_canonical_json(body, "value", ArtifactValidationError)
|
|
271
|
+
schema_key = blob_key(schema_ref)
|
|
272
|
+
try:
|
|
273
|
+
schema_body = datastore.get(schema_key).body
|
|
274
|
+
except DatastoreNotFoundError as error:
|
|
275
|
+
raise ArtifactValidationError(f"schema {schema_ref} is missing") from error
|
|
276
|
+
schema = _parse_canonical_json(schema_body, "schema", ArtifactValidationError)
|
|
277
|
+
if sha256_ref(schema_body) != schema_ref:
|
|
278
|
+
raise ArtifactValidationError(f"schema {schema_ref} does not match its digest")
|
|
279
|
+
if not isinstance(schema, dict):
|
|
280
|
+
raise ArtifactValidationError("schema must be an object")
|
|
281
|
+
try:
|
|
282
|
+
Draft202012Validator.check_schema(schema)
|
|
283
|
+
validator = cast(SchemaValidator, Draft202012Validator(schema))
|
|
284
|
+
except (SchemaError, re.error, Unresolvable) as error:
|
|
285
|
+
raise ArtifactValidationError(f"schema {schema_ref} cannot be used") from error
|
|
286
|
+
try:
|
|
287
|
+
validator.validate(document)
|
|
288
|
+
except ValidationError as error:
|
|
289
|
+
raise ArtifactValidationError(f"value does not satisfy schema {schema_ref}") from error
|
|
290
|
+
except (re.error, Unresolvable) as error:
|
|
291
|
+
raise ArtifactValidationError(f"schema {schema_ref} cannot be used") from error
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _parse_canonical_json(body: bytes, label: str, error_type: type[ArtifactError]) -> JsonValue:
|
|
295
|
+
try:
|
|
296
|
+
return parse_canonical_json(body)
|
|
297
|
+
except CanonicalJsonError as error:
|
|
298
|
+
raise error_type(f"{label} is not canonical JSON") from error
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _canonical_manifest(manifest: dict[str, JsonValue]) -> bytes:
|
|
302
|
+
try:
|
|
303
|
+
_manifest_validator().validate(manifest)
|
|
304
|
+
return canonical_json(manifest).encode()
|
|
305
|
+
except (TypeError, ValueError, ValidationError) as error:
|
|
306
|
+
raise ArtifactValidationError("artifact manifest does not satisfy its schema") from error
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def put_immutable(
|
|
310
|
+
datastore: Datastore,
|
|
311
|
+
key: str,
|
|
312
|
+
body: bytes,
|
|
313
|
+
content_type: str,
|
|
314
|
+
conflict_error: type[ArtifactBodyConflictError | ArtifactManifestConflictError],
|
|
315
|
+
) -> None:
|
|
316
|
+
try:
|
|
317
|
+
datastore.put(key, body, content_type=content_type, if_absent=True)
|
|
318
|
+
return
|
|
319
|
+
except DatastoreConflictError:
|
|
320
|
+
pass
|
|
321
|
+
try:
|
|
322
|
+
existing = datastore.get(key)
|
|
323
|
+
except DatastoreNotFoundError as error:
|
|
324
|
+
raise conflict_error(f"cannot inspect existing immutable object {key}") from error
|
|
325
|
+
if existing.info.content_type != content_type or existing.body != body:
|
|
326
|
+
raise conflict_error(f"existing immutable object {key} differs")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def blob_key(hash_ref: str) -> str:
|
|
330
|
+
prefix = "sha256:"
|
|
331
|
+
digest = hash_ref.removeprefix(prefix)
|
|
332
|
+
if (
|
|
333
|
+
not hash_ref.startswith(prefix)
|
|
334
|
+
or len(digest) != 64
|
|
335
|
+
or any(char not in "0123456789abcdef" for char in digest)
|
|
336
|
+
):
|
|
337
|
+
raise ArtifactValidationError(f"invalid SHA-256 reference {hash_ref!r}")
|
|
338
|
+
return f"blobs/sha256/{digest}"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@cache
|
|
342
|
+
def _manifest_validator() -> SchemaValidator:
|
|
343
|
+
source = importlib.resources.files("massive").joinpath(
|
|
344
|
+
"schemas", "data-artifact-manifest.schema.json"
|
|
345
|
+
)
|
|
346
|
+
if source.is_file():
|
|
347
|
+
document = json.loads(source.read_text(encoding="utf-8"))
|
|
348
|
+
else:
|
|
349
|
+
document = json.loads(
|
|
350
|
+
(
|
|
351
|
+
Path(__file__).resolve().parents[4]
|
|
352
|
+
/ "conformance/schema/data-artifact-manifest.schema.json"
|
|
353
|
+
).read_text()
|
|
354
|
+
)
|
|
355
|
+
Draft202012Validator.check_schema(document)
|
|
356
|
+
return cast(SchemaValidator, Draft202012Validator(document))
|