lean-runtime 2.0.6__tar.gz → 2.0.7__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.
Files changed (146) hide show
  1. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/CHANGELOG.md +6 -0
  2. {lean_runtime-2.0.6/lean_runtime.egg-info → lean_runtime-2.0.7}/PKG-INFO +1 -1
  3. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/oci.py +66 -44
  4. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/run_cli.py +1 -0
  5. {lean_runtime-2.0.6 → lean_runtime-2.0.7/lean_runtime.egg-info}/PKG-INFO +1 -1
  6. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/pyproject.toml +1 -1
  7. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_bundles.py +52 -2
  8. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/LICENSE +0 -0
  9. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/MANIFEST.in +0 -0
  10. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/README.md +0 -0
  11. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/catalog/environments.toml +0 -0
  12. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/catalog/locks/core-v4.32.2.lock.json +0 -0
  13. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
  14. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
  15. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
  16. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/compatibility/README.md +0 -0
  17. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/compatibility/mathlib-4.32.2.json +0 -0
  18. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/compatibility/mathlib-4.32.2.toml +0 -0
  19. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/architecture.md +0 -0
  20. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/captures.md +0 -0
  21. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/case-study-v1.md +0 -0
  22. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/cli.md +0 -0
  23. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/development.md +0 -0
  24. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/environments.md +0 -0
  25. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/getting-started.md +0 -0
  26. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/index.md +0 -0
  27. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/local-projects.md +0 -0
  28. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/portable-copies.md +0 -0
  29. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/python-api.md +0 -0
  30. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/ready-programs.md +0 -0
  31. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/standalone-files.md +0 -0
  32. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/trust-and-limitations.md +0 -0
  33. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/docs/v1-precision.md +0 -0
  34. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/examples/mathlib.toml +0 -0
  35. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/__init__.py +0 -0
  36. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/__main__.py +0 -0
  37. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/_git.py +0 -0
  38. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/_paths.py +0 -0
  39. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/backends.py +0 -0
  40. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/bundles.py +0 -0
  41. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/cli.py +0 -0
  42. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/comparison.py +0 -0
  43. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/decisions.py +0 -0
  44. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/diagnostics.py +0 -0
  45. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/__init__.py +0 -0
  46. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/analyzer.py +0 -0
  47. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/api.py +0 -0
  48. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/candidate.py +0 -0
  49. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/catalog.py +0 -0
  50. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/catalog_build.py +0 -0
  51. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/catalog_cli.py +0 -0
  52. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/catalog_manifest.py +0 -0
  53. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/data/catalog.json +0 -0
  54. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/defaults.py +0 -0
  55. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/engine.py +0 -0
  56. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/errors.py +0 -0
  57. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/module_inventory.py +0 -0
  58. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/planner.py +0 -0
  59. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/policy.py +0 -0
  60. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/probe.py +0 -0
  61. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/py.typed +0 -0
  62. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/result.py +0 -0
  63. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/schema_resources.py +0 -0
  64. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
  65. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
  66. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
  67. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/environments.py +0 -0
  68. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/errors.py +0 -0
  69. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/events.py +0 -0
  70. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/facade.py +0 -0
  71. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/frontmatter.py +0 -0
  72. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/health.py +0 -0
  73. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/lake.py +0 -0
  74. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/lockfiles.py +0 -0
  75. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/locking.py +0 -0
  76. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/matrix.py +0 -0
  77. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/models.py +0 -0
  78. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/policies.py +0 -0
  79. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/profiling.py +0 -0
  80. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/programs.py +0 -0
  81. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/projects.py +0 -0
  82. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/publisher_verification.py +0 -0
  83. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/py.typed +0 -0
  84. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/references.py +0 -0
  85. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/resolver.py +0 -0
  86. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/runtime.py +0 -0
  87. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/schema_resources.py +0 -0
  88. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/serialization.py +0 -0
  89. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/specs.py +0 -0
  90. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/store.py +0 -0
  91. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/timings.py +0 -0
  92. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/toolchains.py +0 -0
  93. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/verification.py +0 -0
  94. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime/wire.py +0 -0
  95. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime.egg-info/SOURCES.txt +0 -0
  96. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime.egg-info/dependency_links.txt +0 -0
  97. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime.egg-info/entry_points.txt +0 -0
  98. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime.egg-info/requires.txt +0 -0
  99. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/lean_runtime.egg-info/top_level.txt +0 -0
  100. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/mkdocs.yml +0 -0
  101. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/schemas/cleanup-v1.schema.json +0 -0
  102. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/schemas/comparison-v1.schema.json +0 -0
  103. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/schemas/execution-v1.schema.json +0 -0
  104. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/schemas/inspect-v1.schema.json +0 -0
  105. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/schemas/matrix-v1.schema.json +0 -0
  106. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/schemas/profile-v1.schema.json +0 -0
  107. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/schemas/verify-v1.schema.json +0 -0
  108. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/scripts/run_compatibility.py +0 -0
  109. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/scripts/run_v1_case_study.py +0 -0
  110. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/scripts/smoke_wheel.py +0 -0
  111. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/setup.cfg +0 -0
  112. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/conftest.py +0 -0
  113. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/discovery/e2e/test_real_discovery.py +0 -0
  114. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/discovery/test_analyzer.py +0 -0
  115. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/discovery/test_catalog.py +0 -0
  116. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/discovery/test_catalog_build.py +0 -0
  117. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/discovery/test_discovery_schema_resources.py +0 -0
  118. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/discovery/test_engine.py +0 -0
  119. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/discovery/test_planner.py +0 -0
  120. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/discovery/test_policy.py +0 -0
  121. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_backend.py +0 -0
  122. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_cli.py +0 -0
  123. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_diagnostics.py +0 -0
  124. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_environment_integration.py +0 -0
  125. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_events_health.py +0 -0
  126. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_facade.py +0 -0
  127. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_frontmatter.py +0 -0
  128. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_git.py +0 -0
  129. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_interactive.py +0 -0
  130. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_lockfiles.py +0 -0
  131. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_multifile.py +0 -0
  132. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_paths.py +0 -0
  133. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_prebuilt_policy.py +0 -0
  134. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_programs.py +0 -0
  135. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_projects.py +0 -0
  136. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_references.py +0 -0
  137. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_run_cli.py +0 -0
  138. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_runtime.py +0 -0
  139. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_schema_resources.py +0 -0
  140. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_schemas.py +0 -0
  141. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_signatures.py +0 -0
  142. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_specs.py +0 -0
  143. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_store.py +0 -0
  144. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_toolchains.py +0 -0
  145. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_v1_precision.py +0 -0
  146. {lean_runtime-2.0.6 → lean_runtime-2.0.7}/tests/test_verification_inventory.py +0 -0
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.0.7
6
+
7
+ - Retry an OCI layer once from a clean partial after size or SHA-256 integrity
8
+ verification fails, bypassing intermediary caches while preserving strict
9
+ digest validation and emitting structured retry diagnostics.
10
+
5
11
  ## 2.0.6
6
12
 
7
13
  - Preserve the highest-ranked Lean compiler rejection when automatic discovery
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.6
3
+ Version: 2.0.7
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -32,6 +32,7 @@ _BEARER_PARAMETER = re.compile(r'([a-zA-Z]+)="([^"]*)"')
32
32
  _DIGEST = re.compile(r"sha256:([0-9a-f]{64})")
33
33
  _ACCEPT = ", ".join((INDEX_MEDIA_TYPE, MANIFEST_MEDIA_TYPE))
34
34
  DEFAULT_ENVIRONMENT_LIBRARIES = ("oci://ghcr.io/alerad/lean-runtime-cache",)
35
+ _BLOB_INTEGRITY_ATTEMPTS = 2
35
36
 
36
37
 
37
38
  class SignatureVerifier(Protocol):
@@ -215,52 +216,73 @@ class OCIRegistryClient:
215
216
  return destination
216
217
  destination.unlink()
217
218
  temporary = destination.with_name(f".{destination.name}.partial")
218
- if temporary.exists() and temporary.stat().st_size > size:
219
- temporary.unlink()
220
- offset = temporary.stat().st_size if temporary.exists() else 0
221
- request = urllib.request.Request(self._url(f"blobs/{digest}"))
222
- if offset:
223
- request.add_header("Range", f"bytes={offset}-")
224
- events.emit(
225
- "library.layer_download_started",
226
- "Downloading OCI blob",
227
- digest=digest,
228
- size=size,
229
- )
230
- observed = hashlib.sha256()
231
- written = offset
232
- if offset:
233
- with temporary.open("rb") as existing:
234
- for chunk in iter(lambda: existing.read(1024 * 1024), b""):
235
- observed.update(chunk)
236
- try:
237
- response = self._request(request)
238
- if offset and response.status != 206:
239
- offset = 0
240
- written = 0
241
- observed = hashlib.sha256()
219
+ for attempt in range(1, _BLOB_INTEGRITY_ATTEMPTS + 1):
220
+ if temporary.exists() and temporary.stat().st_size > size:
221
+ temporary.unlink()
222
+ offset = temporary.stat().st_size if temporary.exists() else 0
223
+ request = urllib.request.Request(self._url(f"blobs/{digest}"))
224
+ if offset:
225
+ request.add_header("Range", f"bytes={offset}-")
226
+ if attempt > 1:
227
+ request.add_header("Cache-Control", "no-cache")
228
+ events.emit(
229
+ "library.layer_download_started",
230
+ "Downloading OCI blob",
231
+ digest=digest,
232
+ size=size,
233
+ attempt=attempt,
234
+ resumed_bytes=offset,
235
+ )
236
+ observed = hashlib.sha256()
237
+ written = offset
242
238
  if offset:
243
- content_range = response.headers.get("Content-Range", "")
244
- if not content_range.startswith(f"bytes {offset}-"):
245
- response.close()
246
- temporary.unlink(missing_ok=True)
247
- raise EnvironmentError("OCI registry returned an invalid byte range")
248
- mode = "ab" if offset else "wb"
249
- with response, temporary.open(mode) as output:
250
- while chunk := response.read(1024 * 1024):
251
- written += len(chunk)
252
- if written > size:
253
- raise EnvironmentError("OCI blob exceeds its declared size")
254
- observed.update(chunk)
255
- output.write(chunk)
256
- if written != size or "sha256:" + observed.hexdigest() != digest:
239
+ with temporary.open("rb") as existing:
240
+ for chunk in iter(lambda: existing.read(1024 * 1024), b""):
241
+ observed.update(chunk)
242
+ try:
243
+ response = self._request(request)
244
+ if offset and response.status != 206:
245
+ offset = 0
246
+ written = 0
247
+ observed = hashlib.sha256()
248
+ if offset:
249
+ content_range = response.headers.get("Content-Range", "")
250
+ if not content_range.startswith(f"bytes {offset}-"):
251
+ response.close()
252
+ temporary.unlink(missing_ok=True)
253
+ raise EnvironmentError("OCI registry returned an invalid byte range")
254
+ mode = "ab" if offset else "wb"
255
+ with response, temporary.open(mode) as output:
256
+ while chunk := response.read(1024 * 1024):
257
+ written += len(chunk)
258
+ if written > size:
259
+ raise EnvironmentError("OCI blob exceeds its declared size")
260
+ observed.update(chunk)
261
+ output.write(chunk)
262
+ observed_digest = "sha256:" + observed.hexdigest()
263
+ if written == size and observed_digest == digest:
264
+ temporary.replace(destination)
265
+ break
257
266
  temporary.unlink(missing_ok=True)
258
- raise EnvironmentError("downloaded OCI blob failed digest verification")
259
- temporary.replace(destination)
260
- except urllib.error.HTTPError as exc:
261
- raise DownloadUnavailable(f"OCI blob download failed: HTTP {exc.code}") from exc
262
- except OSError as exc:
263
- raise DownloadUnavailable(f"OCI blob download failed: {exc}") from exc
267
+ if attempt == _BLOB_INTEGRITY_ATTEMPTS:
268
+ raise EnvironmentError(
269
+ "downloaded OCI blob failed digest verification "
270
+ f"after {attempt} attempts (expected {digest}, got "
271
+ f"{observed_digest}, bytes {written}/{size})"
272
+ )
273
+ events.emit(
274
+ "library.layer_download_retry",
275
+ "Retrying OCI blob after integrity verification failed",
276
+ digest=digest,
277
+ attempt=attempt + 1,
278
+ observed_digest=observed_digest,
279
+ downloaded_bytes=written,
280
+ expected_bytes=size,
281
+ )
282
+ except urllib.error.HTTPError as exc:
283
+ raise DownloadUnavailable(f"OCI blob download failed: HTTP {exc.code}") from exc
284
+ except OSError as exc:
285
+ raise DownloadUnavailable(f"OCI blob download failed: {exc}") from exc
264
286
  return destination
265
287
 
266
288
  def blob_exists(self, digest: str) -> bool:
@@ -34,6 +34,7 @@ def _progress(event: RuntimeEvent) -> None:
34
34
  "package_reference.started": "Resolving dependency",
35
35
  "library.lookup": "Looking for a cached environment",
36
36
  "library.layer_download_started": "Downloading cached environment",
37
+ "library.layer_download_retry": "Retrying cached-environment download",
37
38
  "environment.build_started": "Building environment",
38
39
  "environment.cache_hit": "Using local environment",
39
40
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.6
3
+ Version: 2.0.7
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lean-runtime"
7
- version = "2.0.6"
7
+ version = "2.0.7"
8
8
  description = "Run Lean 4 proofs from Python or standalone files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -18,8 +18,14 @@ import pytest
18
18
  from lean_runtime import EnvironmentError, EnvironmentLock, LockedPackage, Runtime
19
19
  from lean_runtime.bundles import _extract_layer, _oci_archive
20
20
  from lean_runtime.environments import ENVIRONMENT_SCHEMA
21
- from lean_runtime.oci import OCIEnvironmentPublisher, OCIRepository
22
- from lean_runtime.store import environment_identity, platform_record, source_snapshot_digest
21
+ from lean_runtime.events import EventEmitter
22
+ from lean_runtime.oci import OCIEnvironmentPublisher, OCIRegistryClient, OCIRepository
23
+ from lean_runtime.store import (
24
+ EnvironmentStore,
25
+ environment_identity,
26
+ platform_record,
27
+ source_snapshot_digest,
28
+ )
23
29
 
24
30
 
25
31
  def _git_package(path: Path) -> tuple[str, str]:
@@ -403,6 +409,50 @@ def test_transparent_authenticated_oci_pull_and_blob_reuse(tmp_path: Path) -> No
403
409
  thread.join(timeout=5)
404
410
 
405
411
 
412
+ def test_oci_blob_integrity_failure_retries_once_from_scratch(tmp_path: Path) -> None:
413
+ data = b"verified layer contents"
414
+ digest = "sha256:" + hashlib.sha256(data).hexdigest()
415
+ requests = 0
416
+ cache_controls: list[str | None] = []
417
+
418
+ class Handler(http.server.BaseHTTPRequestHandler):
419
+ def do_GET(self) -> None:
420
+ nonlocal requests
421
+ requests += 1
422
+ cache_controls.append(self.headers.get("Cache-Control"))
423
+ response = b"x" * len(data) if requests == 1 else data
424
+ self.send_response(200)
425
+ self.send_header("Content-Length", str(len(response)))
426
+ self.end_headers()
427
+ self.wfile.write(response)
428
+
429
+ def log_message(self, _format: str, *_args: object) -> None:
430
+ pass
431
+
432
+ server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
433
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
434
+ thread.start()
435
+ events = []
436
+ try:
437
+ repository = OCIRepository.parse(f"oci+http://127.0.0.1:{server.server_port}/owner/cache")
438
+ path = OCIRegistryClient(repository).download_blob(
439
+ {"digest": digest, "size": len(data)},
440
+ EnvironmentStore(tmp_path / "consumer"),
441
+ EventEmitter(events.append),
442
+ )
443
+ assert path.read_bytes() == data
444
+ assert requests == 2
445
+ assert cache_controls == [None, "no-cache"]
446
+ assert [event.kind for event in events].count("library.layer_download_retry") == 1
447
+ starts = [event for event in events if event.kind == "library.layer_download_started"]
448
+ assert [event.data["attempt"] for event in starts] == [1, 2]
449
+ assert starts[1].data["resumed_bytes"] == 0
450
+ finally:
451
+ server.shutdown()
452
+ server.server_close()
453
+ thread.join(timeout=5)
454
+
455
+
406
456
  def test_oci_publisher_uploads_blobs_before_manifests(tmp_path: Path) -> None:
407
457
  producer, environment_id, lock = _published_runtime(tmp_path / "producer")
408
458
  publisher = OCIEnvironmentPublisher(
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes