pipelex-api 0.92.0__tar.gz → 0.92.2__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.
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/PKG-INFO +2 -2
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/blob_store.py +79 -33
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/method_source.py +4 -1
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/method_tree_request.py +13 -12
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/pipeline.py +7 -5
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/schemas/models.py +4 -3
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/.gitignore +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/LICENSE +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/README.md +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/hatch_build.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/__init__.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/api.toml +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/api_config.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/blob_upload.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/bundle.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/bundle_resources.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/exception_handlers.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/exceptions.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/json_body.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/limits.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/main.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/method_cache.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/middleware.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/openapi_responses.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/openapi_schema.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/problem_document.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/problem_view.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/py.typed +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/request_refusals.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/__init__.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/blob_upload.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/health.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/__init__.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/agent/__init__.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/agent/models.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/blobs.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/codegen.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/crate_ops.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/method_selection.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/pipe_io.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/resolve.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/tools.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/utils.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/pipelex/validate.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/routes/version.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/run_extras_binding.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/schemas/__init__.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pipelex_api/security.py +0 -0
- {pipelex_api-0.92.0 → pipelex_api-0.92.2}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pipelex-api
|
|
3
|
-
Version: 0.92.
|
|
3
|
+
Version: 0.92.2
|
|
4
4
|
Summary: Pipelex API
|
|
5
5
|
Project-URL: Homepage, https://pipelex.com
|
|
6
6
|
Project-URL: Repository, https://github.com/Pipelex/pipelex
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
17
|
Requires-Python: <3.15,>=3.11
|
|
18
18
|
Requires-Dist: fastapi<0.142,>=0.118.0
|
|
19
|
-
Requires-Dist: pipelex[anthropic,bedrock,fal,google,google-genai,mistralai]==0.92.
|
|
19
|
+
Requires-Dist: pipelex[anthropic,bedrock,fal,google,google-genai,mistralai]==0.92.2
|
|
20
20
|
Requires-Dist: pyjwt>=2.10.1
|
|
21
21
|
Requires-Dist: uvicorn>=0.37.0
|
|
22
22
|
Provides-Extra: dev
|
|
@@ -38,21 +38,24 @@ From this the blob store answers without reading what it does not need:
|
|
|
38
38
|
ones held, and nothing is written, while bytes of another size mean the object held is damaged, which only a storage
|
|
39
39
|
that does not publish writes whole could leave, a 500 `pipelex.storage.damaged`, since nothing writes over a blob
|
|
40
40
|
and its operator must remove the object.
|
|
41
|
-
- A run
|
|
42
|
-
|
|
43
|
-
is
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
- A run reads each hash's bytes in one request, a read of at most one byte more than the largest size its entries
|
|
42
|
+
state, so the bytes read are bounded by the sizes the request states, which its census already held to the tree's
|
|
43
|
+
limits, whatever size the object is held at. A hash held at another size than an entry states, which the length read
|
|
44
|
+
shows, is a 422 `request.invalid`, never `request.blobs_missing`, since uploading those bytes again would change
|
|
45
|
+
nothing: the entry is wrong. Only that refusal looks the size held up, for the operator's message, when the read
|
|
46
|
+
stopped short of the object's end.
|
|
47
|
+
Bytes read at the size their entries state are checked against their hash. Bytes that do not hash to their key mean
|
|
48
|
+
the object stored under it is damaged, which no caller can repair and no retry cures: a 500
|
|
49
|
+
`pipelex.storage.damaged` naming the kind of storage, whose verdict is not to retry, and an error record, "Stored blob
|
|
50
|
+
bytes do not match their hash", naming the hash for the operator to remove the object, after which the blob is
|
|
51
|
+
uploaded again.
|
|
50
52
|
|
|
51
53
|
Lookups and reads run a bounded number at a time, so one request naming many blobs never opens them all at once.
|
|
52
54
|
"""
|
|
53
55
|
|
|
54
56
|
from collections.abc import Iterable, Mapping
|
|
55
57
|
from functools import partial
|
|
58
|
+
from typing import NamedTuple
|
|
56
59
|
|
|
57
60
|
from mthds.method_tree.digest import DIGEST_PREFIX, sha256_hex, tree_listing
|
|
58
61
|
from mthds.method_tree.file_entry import BlobStore, InMemoryBlobStore
|
|
@@ -73,9 +76,11 @@ BLOBS_PREFIX = "blobs"
|
|
|
73
76
|
TREES_PREFIX = "trees"
|
|
74
77
|
_BLOB_CONTENT_TYPE = "application/octet-stream"
|
|
75
78
|
_LISTING_CONTENT_TYPE = "text/plain; charset=utf-8"
|
|
76
|
-
# How many lookups of a blob's size, which read metadata alone,
|
|
77
|
-
# request runs at a time.
|
|
79
|
+
# How many lookups of a blob's size, which read metadata alone, how many reads of the blobs a request names, each
|
|
80
|
+
# bounded by the size the request states for it, and how many writes of a blob's bytes one request runs at a time. The
|
|
81
|
+
# S3 provider's pool of connections holds at least as many as the first two (`S3_MAX_POOL_CONNECTIONS`).
|
|
78
82
|
MAX_CONCURRENT_BLOB_LOOKUPS = 16
|
|
83
|
+
MAX_CONCURRENT_BLOB_READS = 16
|
|
79
84
|
MAX_CONCURRENT_BLOB_TRANSFERS = 4
|
|
80
85
|
# The message of the error record a blob read whose bytes do not match their hash writes, for the operator to search.
|
|
81
86
|
BLOB_DAMAGED_MESSAGE = "Stored blob bytes do not match their hash"
|
|
@@ -104,6 +109,16 @@ def problem_scheme_of(*, storage_method: str) -> str:
|
|
|
104
109
|
return PLUGIN_STORAGE_SCHEME
|
|
105
110
|
|
|
106
111
|
|
|
112
|
+
class StatedBlobs(NamedTuple):
|
|
113
|
+
"""The blobs a map of file entries names by hash, as the blob store holds them."""
|
|
114
|
+
|
|
115
|
+
held: BlobStore
|
|
116
|
+
"""The bytes of each hash held at the size its entries state, checked against the hash."""
|
|
117
|
+
|
|
118
|
+
mismatched: dict[str, int]
|
|
119
|
+
"""Each hash held at another size than an entry states, with the size it is held at, in hash order."""
|
|
120
|
+
|
|
121
|
+
|
|
107
122
|
class RunnerBlobStore:
|
|
108
123
|
"""The blobs and tree listings of one caller's scope, over the deployment's storage provider."""
|
|
109
124
|
|
|
@@ -155,37 +170,68 @@ class RunnerBlobStore:
|
|
|
155
170
|
sizes = await gather_bounded([partial(self.stored_size, sha256=sha256) for sha256 in hashes], max_concurrency=MAX_CONCURRENT_BLOB_LOOKUPS)
|
|
156
171
|
return dict(zip(hashes, sizes, strict=True))
|
|
157
172
|
|
|
158
|
-
async def
|
|
159
|
-
"""
|
|
173
|
+
async def _read_bounded(self, *, sha256: str, nb_bytes: int) -> bytes | None:
|
|
174
|
+
"""At most `nb_bytes` leading bytes of the blob `sha256`, read in one request, or None when the hash is not held."""
|
|
175
|
+
try:
|
|
176
|
+
return await self._storage.load_head(f"{PIPELEX_STORAGE_SCHEME}{self._blob_key(sha256=sha256)}", nb_bytes=nb_bytes)
|
|
177
|
+
except StorageFileNotFoundError:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
async def read_stated(self, *, stated_sizes: Mapping[str, set[int]]) -> StatedBlobs:
|
|
181
|
+
"""Read the blobs a map of file entries names, each in one request bounded by the sizes its entries state.
|
|
182
|
+
|
|
183
|
+
Each hash is read for at most one byte more than the largest size its entries state, so a hash held at a larger
|
|
184
|
+
size shows it without its bytes being read, and the bytes read stay bounded by the sizes stated. A hash not
|
|
185
|
+
held is left out, for the reader to report among the blobs this runner does not hold.
|
|
160
186
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
187
|
+
Args:
|
|
188
|
+
stated_sizes: Each hash the map names, with every size its entries state for it, which the map's census
|
|
189
|
+
held to the tree's limits.
|
|
190
|
+
|
|
191
|
+
Raises (as a problem document): a 500 `pipelex.storage.damaged` naming the kind of storage, when no hash is
|
|
192
|
+
held at another size and bytes read at the size their entries state do not hash to their key, after an error
|
|
193
|
+
record naming each such hash, the one place the operator reads which damaged object to remove.
|
|
164
194
|
"""
|
|
165
|
-
|
|
166
|
-
|
|
195
|
+
hashes = sorted(stated_sizes)
|
|
196
|
+
reads = await gather_bounded(
|
|
197
|
+
[partial(self._read_bounded, sha256=sha256, nb_bytes=max(stated_sizes[sha256]) + 1) for sha256 in hashes],
|
|
198
|
+
max_concurrency=MAX_CONCURRENT_BLOB_READS,
|
|
199
|
+
)
|
|
200
|
+
blobs: dict[str, bytes] = {}
|
|
201
|
+
mismatched: dict[str, int] = {}
|
|
202
|
+
for sha256, data in zip(hashes, reads, strict=True):
|
|
203
|
+
if data is None:
|
|
204
|
+
continue
|
|
205
|
+
sizes = stated_sizes[sha256]
|
|
206
|
+
if sizes == {len(data)}:
|
|
207
|
+
blobs[sha256] = data
|
|
208
|
+
elif len(data) <= max(sizes):
|
|
209
|
+
mismatched[sha256] = len(data)
|
|
210
|
+
else:
|
|
211
|
+
# The read stopped a byte past the largest size stated: the object is larger, and its size is looked up.
|
|
212
|
+
held_size = await self.stored_size(sha256=sha256)
|
|
213
|
+
if held_size is not None:
|
|
214
|
+
mismatched[sha256] = held_size
|
|
215
|
+
if mismatched:
|
|
216
|
+
return StatedBlobs(held=InMemoryBlobStore(), mismatched=mismatched)
|
|
217
|
+
damaged = [sha256 for sha256, data in blobs.items() if sha256_hex(data) != sha256]
|
|
218
|
+
for sha256 in damaged:
|
|
167
219
|
log.error(
|
|
168
220
|
BLOB_DAMAGED_MESSAGE,
|
|
169
|
-
fields={
|
|
221
|
+
fields={
|
|
222
|
+
"blob_sha256": sha256,
|
|
223
|
+
"blob_size_bytes": len(blobs[sha256]),
|
|
224
|
+
"read_size_bytes": len(blobs[sha256]),
|
|
225
|
+
"storage_scheme": self._scheme,
|
|
226
|
+
},
|
|
170
227
|
)
|
|
228
|
+
if damaged:
|
|
171
229
|
msg = (
|
|
172
|
-
f"The bytes this runner's blob store holds under {
|
|
230
|
+
f"The bytes this runner's blob store holds under {damaged[0]} do not hash to it: the storage holds a damaged "
|
|
173
231
|
"object, which its operator removes for the blob to be uploaded again."
|
|
174
232
|
)
|
|
175
233
|
raise BlobStoreDamagedError(msg, scheme=self._scheme)
|
|
176
|
-
return
|
|
177
|
-
|
|
178
|
-
async def read_held(self, *, sizes: Mapping[str, int]) -> BlobStore:
|
|
179
|
-
"""Read these held blobs, each at its stored size, as the synchronous blob store a map of file entries is read with.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
sizes: Each hash to read, with the size it is held at, which the caller bounded before asking.
|
|
183
|
-
"""
|
|
184
|
-
blobs = await gather_bounded(
|
|
185
|
-
[partial(self._read_verified, sha256=sha256, size=size) for sha256, size in sizes.items()],
|
|
186
|
-
max_concurrency=MAX_CONCURRENT_BLOB_TRANSFERS,
|
|
187
|
-
)
|
|
188
|
-
return InMemoryBlobStore(blobs)
|
|
234
|
+
return StatedBlobs(held=InMemoryBlobStore(list(blobs.values())), mismatched={})
|
|
189
235
|
|
|
190
236
|
async def store_blob(self, *, data: bytes) -> str:
|
|
191
237
|
"""Hold `data` under its SHA-256, creating the blob unless it is held already, and return the hash.
|
|
@@ -13,7 +13,8 @@ commit, read from the commit's git objects as stored, with no checkout filter, l
|
|
|
13
13
|
`export-ignore`, so its digest is the same on every machine, and filtered by the membership rule read with that
|
|
14
14
|
commit's own `.gitignore` files and the package's own `exclude` patterns. A submodule contributes no file, and a
|
|
15
15
|
symbolic link committed in the package is refused unless the rule excludes it. The tree's digest is recorded in the
|
|
16
|
-
run's provenance (`method_provenance.tree_digest`), beside the commit
|
|
16
|
+
run's provenance (`method_provenance.tree_digest`), beside the commit and the `display_name` the package's manifest
|
|
17
|
+
declares, which the fetched package's own provenance already carries.
|
|
17
18
|
|
|
18
19
|
The packages a package ships under its own `.mthds/methods/<name>/`, the layout of the local method cache, are out of
|
|
19
20
|
its tree (a dot directory), and each becomes a dependency tree of its own, keyed by the address its manifest declares,
|
|
@@ -291,5 +292,7 @@ def fetched_method(method_ref: str) -> FetchedMethod:
|
|
|
291
292
|
"""
|
|
292
293
|
with _fetched_package(method_ref) as package:
|
|
293
294
|
request_method = _read_package_method(package=package)
|
|
295
|
+
# The package's own provenance already holds the address, the tag, the commit and the manifest's display name;
|
|
296
|
+
# only the digest of the tree this runner read is added here.
|
|
294
297
|
provenance = package.provenance.model_copy(update={"tree_digest": request_method.tree.tree.digest})
|
|
295
298
|
return FetchedMethod(request_method=request_method, package=package, provenance=provenance)
|
|
@@ -419,26 +419,27 @@ def _refuse_map(*, refusal_error: MethodTreeRefusedError, label: str, field: str
|
|
|
419
419
|
|
|
420
420
|
|
|
421
421
|
async def _held_blobs(*, blob_store: RunnerBlobStore, stated_sizes: dict[str, set[int]], label: str, field: str) -> BlobStore:
|
|
422
|
-
"""The bytes a map's hash entries name that the blob store holds,
|
|
422
|
+
"""The bytes a map's hash entries name that the blob store holds, each read in one request bounded by its stated size.
|
|
423
423
|
|
|
424
424
|
A hash held at another size than one of its entries states is refused, since uploading its bytes again would not
|
|
425
|
-
change the size the store holds them at. Every
|
|
426
|
-
census bounded, so reading them reads no more than the census allowed. A hash not held is left
|
|
427
|
-
to report among the blobs this runner does not hold.
|
|
428
|
-
|
|
429
|
-
Raises (as
|
|
430
|
-
diagnostics, lists each hash held at another size, with that size, which no parameter of the code
|
|
425
|
+
change the size the store holds them at. Every hash is read for at most one byte past the largest size its entries
|
|
426
|
+
state, which the census bounded, so reading them reads no more than the census allowed. A hash not held is left
|
|
427
|
+
out, for the reader to report among the blobs this runner does not hold.
|
|
428
|
+
|
|
429
|
+
Raises (as problem documents): a 422 `request.invalid` naming the field the map rides, whose message, in the
|
|
430
|
+
operator's diagnostics, lists each hash held at another size, with that size, which no parameter of the code
|
|
431
|
+
carries; and, when no hash is, the 500 `pipelex.storage.damaged` of the blob store for bytes that do not hash to
|
|
432
|
+
their key.
|
|
431
433
|
"""
|
|
432
|
-
|
|
433
|
-
mismatched
|
|
434
|
-
|
|
435
|
-
described = ", ".join(f"{sha256} (held at {size} bytes)" for sha256, size in mismatched.items())
|
|
434
|
+
stated = await blob_store.read_stated(stated_sizes=stated_sizes)
|
|
435
|
+
if stated.mismatched:
|
|
436
|
+
described = ", ".join(f"{sha256} (held at {size} bytes)" for sha256, size in stated.mismatched.items())
|
|
436
437
|
msg = (
|
|
437
438
|
f"{label} states a size for a hash other than the size of the bytes this runner holds under it: {described}. "
|
|
438
439
|
"A hash names one set of bytes, so state the size of those bytes."
|
|
439
440
|
)
|
|
440
441
|
raise RunnerRequestError(msg, field=field)
|
|
441
|
-
return
|
|
442
|
+
return stated.held
|
|
442
443
|
|
|
443
444
|
|
|
444
445
|
async def read_request_tree(
|
|
@@ -1043,8 +1043,9 @@ async def execute(request: Request) -> JSONResponse:
|
|
|
1043
1043
|
# `TokensUsageRecord` wire shape instead (pipelex owns the shape authority).
|
|
1044
1044
|
response_dump = response.model_dump(mode="json", serialize_as_any=True, by_alias=True)
|
|
1045
1045
|
apply_tokens_usage_wire_shape(response_dump, pipe_output=response.pipe_output)
|
|
1046
|
-
# Provenance of a `method_ref` run (address, tag, resolved commit SHA
|
|
1047
|
-
# the run was selected by reference, so the classic response
|
|
1046
|
+
# Provenance of a `method_ref` run (address, tag, resolved commit SHA, tree digest, the manifest's
|
|
1047
|
+
# display name) — attached only when the run was selected by reference, so the classic response
|
|
1048
|
+
# stays byte-identical.
|
|
1048
1049
|
if source.method_provenance is not None:
|
|
1049
1050
|
response_dump["method_provenance"] = source.method_provenance.model_dump(mode="json")
|
|
1050
1051
|
return JSONResponse(content=response_dump)
|
|
@@ -1140,9 +1141,10 @@ async def start(
|
|
|
1140
1141
|
requested_orchestration_mode=extras.orchestration_mode,
|
|
1141
1142
|
callback_problem_view=problem_view_of(request),
|
|
1142
1143
|
)
|
|
1143
|
-
# The ack plus this server's provenance extension: `(address, tag, commit_sha
|
|
1144
|
-
# `method_ref` run, null otherwise — the caller (and, on hosted, the
|
|
1145
|
-
# learns exactly what was fetched
|
|
1144
|
+
# The ack plus this server's provenance extension: `(address, tag, commit_sha, tree_digest,
|
|
1145
|
+
# display_name)` for a `method_ref` run, null otherwise — the caller (and, on hosted, the
|
|
1146
|
+
# platform's Run row) learns exactly what was fetched, at which commit, and what the package
|
|
1147
|
+
# calls itself.
|
|
1146
1148
|
return PipelexApiStartResponse(
|
|
1147
1149
|
**dict(start_result),
|
|
1148
1150
|
method_provenance=source.method_provenance,
|
|
@@ -615,9 +615,10 @@ class PipeOutputWire(PipeOutput):
|
|
|
615
615
|
|
|
616
616
|
_METHOD_PROVENANCE_DESCRIPTION = (
|
|
617
617
|
"PIPELEX-API EXTENSION (not part of the MTHDS Protocol) — provenance of a `method_ref` run: the package's "
|
|
618
|
-
"resolved full address, the requested tag (null for a bare address),
|
|
619
|
-
"fetched
|
|
620
|
-
"
|
|
618
|
+
"resolved full address, the requested tag (null for a bare address), the commit SHA that was actually "
|
|
619
|
+
"fetched, the digest of the method tree the run loaded, and the `display_name` the package's manifest declares "
|
|
620
|
+
"(null when it declares none). The SHA is what keeps the run explainable when a tag moves. Absent (or null) for "
|
|
621
|
+
"runs from inline source or a bundle."
|
|
621
622
|
)
|
|
622
623
|
|
|
623
624
|
|
|
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
|
|
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
|