lean-runtime 2.3.0__tar.gz → 2.4.0__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.
- {lean_runtime-2.3.0/lean_runtime.egg-info → lean_runtime-2.4.0}/PKG-INFO +1 -1
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/__init__.py +2 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/cli.py +129 -15
- lean_runtime-2.4.0/lean_runtime/console.py +246 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/probe.py +8 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/errors.py +9 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/events.py +28 -3
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/oci.py +86 -6
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/policies.py +42 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/run_cli.py +123 -33
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/runtime.py +62 -1
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/store.py +119 -4
- {lean_runtime-2.3.0 → lean_runtime-2.4.0/lean_runtime.egg-info}/PKG-INFO +1 -1
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime.egg-info/SOURCES.txt +2 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/pyproject.toml +1 -1
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_bundles.py +110 -1
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_cli.py +54 -0
- lean_runtime-2.4.0/tests/test_console.py +251 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_run_cli.py +142 -1
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_store.py +46 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/CHANGELOG.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/LICENSE +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/MANIFEST.in +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/README.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/environments.toml +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/core-v4.32.2.lock.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/leancert-v4.30.0.5.lock.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/leancert-v4.31.0.lock.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/leancert-v4.32.2.4.lock.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/leancert-v4.33.0.lock.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/mathlib-v4.33.0.lock.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/compatibility/README.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/compatibility/mathlib-4.32.2.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/compatibility/mathlib-4.32.2.toml +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/compatibility/mathlib-4.33.0.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/compatibility/mathlib-4.33.0.toml +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/architecture.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/captures.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/case-study-v1.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/cli.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/development.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/environments.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/getting-started.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/index.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/local-projects.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/portable-copies.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/project-publishing.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/python-api.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/ready-programs.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/standalone-files.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/trust-and-limitations.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/v1-precision.md +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/examples/mathlib.toml +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/__main__.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/_git.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/_paths.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/backends.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/bundles.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/comparison.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/decisions.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/diagnostics.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/__init__.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/analyzer.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/api.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/candidate.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/catalog.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/catalog_build.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/catalog_cli.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/catalog_manifest.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/data/catalog.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/defaults.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/engine.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/errors.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/module_inventory.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/planner.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/policy.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/py.typed +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/result.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/schema_resources.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/environments.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/facade.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/frontmatter.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/health.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/lake.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/lockfiles.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/locking.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/matrix.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/models.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/profiling.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/programs.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/projects.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/publisher_verification.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/py.typed +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/references.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/resolver.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/schema_resources.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/serialization.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/specs.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/timings.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/toolchain_slim.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/toolchains.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/verification.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/wire.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime.egg-info/dependency_links.txt +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime.egg-info/entry_points.txt +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime.egg-info/requires.txt +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime.egg-info/top_level.txt +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/mkdocs.yml +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/cleanup-v1.schema.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/comparison-v1.schema.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/execution-v1.schema.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/inspect-v1.schema.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/matrix-v1.schema.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/profile-v1.schema.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/verify-v1.schema.json +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/scripts/announcement_gate.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/scripts/registry_preflight.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/scripts/run_compatibility.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/scripts/run_v1_case_study.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/scripts/smoke_wheel.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/setup.cfg +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/conftest.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/e2e/test_real_discovery.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_analyzer.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_catalog.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_catalog_build.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_discovery_schema_resources.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_engine.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_planner.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_policy.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_backend.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_diagnostics.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_environment_integration.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_events_health.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_facade.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_frontmatter.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_git.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_interactive.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_lockfiles.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_models.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_multifile.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_paths.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_prebuilt_policy.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_programs.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_project_publish.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_projects.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_references.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_runtime.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_schema_resources.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_schemas.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_signatures.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_specs.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_toolchain_slim.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_toolchains.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_v1_precision.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_verification_inventory.py +0 -0
- {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_verification_probe.py +0 -0
|
@@ -13,6 +13,7 @@ from .environments import (
|
|
|
13
13
|
InteractiveSession,
|
|
14
14
|
)
|
|
15
15
|
from .errors import (
|
|
16
|
+
DownloadLimitExceeded,
|
|
16
17
|
DownloadUnavailable,
|
|
17
18
|
EnvironmentError,
|
|
18
19
|
LeanCheckError,
|
|
@@ -76,6 +77,7 @@ __all__ = [
|
|
|
76
77
|
"ComparisonEntry",
|
|
77
78
|
"PortableCopyInfo",
|
|
78
79
|
"DownloadCleanupReport",
|
|
80
|
+
"DownloadLimitExceeded",
|
|
79
81
|
"DEFAULT_ENVIRONMENT_LIBRARIES",
|
|
80
82
|
"DependencyInput",
|
|
81
83
|
"DiscoveredPackage",
|
|
@@ -6,19 +6,22 @@ import argparse
|
|
|
6
6
|
import json
|
|
7
7
|
import sys
|
|
8
8
|
import time
|
|
9
|
+
from datetime import datetime
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
13
|
+
from .console import styler_for
|
|
12
14
|
from .environments import ExecutionCapture
|
|
13
15
|
from .errors import LeanRuntimeError, MaterializationError, ResolutionError
|
|
14
16
|
from .events import RuntimeEvent
|
|
15
17
|
from .lockfiles import EnvironmentLock
|
|
16
18
|
from .matrix import load_matrix
|
|
17
19
|
from .models import ExecutionResult, PhaseTiming
|
|
18
|
-
from .policies import ExecutionPolicy
|
|
20
|
+
from .policies import ExecutionPolicy, format_byte_size
|
|
19
21
|
from .projects import discover_project, project_publication_workflow
|
|
20
22
|
from .runtime import Runtime
|
|
21
23
|
from .specs import EnvironmentSpec
|
|
24
|
+
from .store import CleanupReport, DownloadCleanupReport, StoreStatus
|
|
22
25
|
from .timings import render_timings
|
|
23
26
|
from .wire import (
|
|
24
27
|
envelope,
|
|
@@ -46,6 +49,105 @@ def _json(value: Any) -> None:
|
|
|
46
49
|
print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True))
|
|
47
50
|
|
|
48
51
|
|
|
52
|
+
def _relative_age(timestamp: str | None) -> str:
|
|
53
|
+
if not timestamp:
|
|
54
|
+
return "unknown"
|
|
55
|
+
try:
|
|
56
|
+
moment = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
|
57
|
+
except ValueError:
|
|
58
|
+
return "unknown"
|
|
59
|
+
seconds = max(0.0, time.time() - moment.timestamp())
|
|
60
|
+
for unit_seconds, label in ((31_536_000, "y"), (2_592_000, "mo"), (86_400, "d"), (3_600, "h")):
|
|
61
|
+
if seconds >= unit_seconds:
|
|
62
|
+
return f"{int(seconds // unit_seconds)}{label} ago"
|
|
63
|
+
return "recent"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _render_storage(status: StoreStatus) -> None:
|
|
67
|
+
style = styler_for(sys.stdout)
|
|
68
|
+
print(f"{style.bold('Store')} {status.home}")
|
|
69
|
+
print()
|
|
70
|
+
rows = (
|
|
71
|
+
("Environments", status.environments, status.environments_bytes),
|
|
72
|
+
("Sources", status.sources, status.sources_bytes),
|
|
73
|
+
("Download cache", status.oci_blobs, status.oci_blobs_bytes),
|
|
74
|
+
("Toolchains", None, status.toolchains_bytes),
|
|
75
|
+
("Executions", status.executions, status.executions_bytes),
|
|
76
|
+
)
|
|
77
|
+
for label, count, bytes_used in rows:
|
|
78
|
+
counted = f"{count:>5}" if count is not None else " "
|
|
79
|
+
size_column = style.cyan(f"{format_byte_size(bytes_used):>10}")
|
|
80
|
+
print(f" {label:<15}{counted} {size_column}")
|
|
81
|
+
total_label = style.bold(f"{'Total':<15}")
|
|
82
|
+
total_size = style.bold(f"{format_byte_size(status.bytes_used):>10}")
|
|
83
|
+
free_note = style.dim(f"({format_byte_size(status.bytes_free)} free on disk)")
|
|
84
|
+
print(f" {total_label} {total_size} {free_note}")
|
|
85
|
+
if status.environment_usage:
|
|
86
|
+
print()
|
|
87
|
+
print(style.bold("Largest environments"))
|
|
88
|
+
for usage in status.environment_usage[:5]:
|
|
89
|
+
name = ", ".join(usage.aliases) if usage.aliases else usage.environment_id[:16] + "…"
|
|
90
|
+
details = " · ".join(
|
|
91
|
+
part
|
|
92
|
+
for part in (usage.toolchain, f"last used {_relative_age(usage.last_used_at)}")
|
|
93
|
+
if part
|
|
94
|
+
)
|
|
95
|
+
size_column = style.cyan(f"{format_byte_size(usage.bytes_used):>10}")
|
|
96
|
+
print(f" {size_column} {name} {style.dim(details)}")
|
|
97
|
+
print()
|
|
98
|
+
print(style.dim("Reclaim space: lean-runtime clean (preview, keeps recent/named)"))
|
|
99
|
+
print(style.dim(" lean-runtime clean --execute --include-downloads"))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _render_cleanup(environments: CleanupReport, downloads: DownloadCleanupReport | None) -> None:
|
|
103
|
+
style = styler_for(sys.stdout)
|
|
104
|
+
retained_note = style.dim(
|
|
105
|
+
f"Retained {len(environments.retained)} environment(s) (named, recent, or in use)."
|
|
106
|
+
)
|
|
107
|
+
if environments.dry_run:
|
|
108
|
+
anything = False
|
|
109
|
+
if environments.candidates:
|
|
110
|
+
anything = True
|
|
111
|
+
print(
|
|
112
|
+
style.bold(
|
|
113
|
+
f"Would remove {len(environments.candidates)} unused environment(s) · "
|
|
114
|
+
f"{format_byte_size(environments.candidate_bytes)}"
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
for name in environments.candidates:
|
|
118
|
+
print(f" {style.dim(name)}")
|
|
119
|
+
else:
|
|
120
|
+
print("No unused environments to remove.")
|
|
121
|
+
if downloads is not None:
|
|
122
|
+
if downloads.candidates:
|
|
123
|
+
anything = True
|
|
124
|
+
print(
|
|
125
|
+
style.bold(
|
|
126
|
+
f"Would remove {len(downloads.candidates)} cached download(s) · "
|
|
127
|
+
f"{format_byte_size(downloads.candidate_bytes)}"
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
print("No unreferenced cached downloads to remove.")
|
|
132
|
+
print(retained_note)
|
|
133
|
+
if anything:
|
|
134
|
+
print()
|
|
135
|
+
print("This was a preview. Re-run with --execute to delete.")
|
|
136
|
+
return
|
|
137
|
+
reclaimed = environments.reclaimed_bytes + (downloads.reclaimed_bytes if downloads else 0)
|
|
138
|
+
removed = len(environments.removed) + (len(downloads.removed) if downloads else 0)
|
|
139
|
+
if removed:
|
|
140
|
+
parts = [f"{len(environments.removed)} environment(s)"]
|
|
141
|
+
if downloads is not None:
|
|
142
|
+
parts.append(f"{len(downloads.removed)} cached download(s)")
|
|
143
|
+
print(
|
|
144
|
+
style.green(f"Removed {' and '.join(parts)} · reclaimed {format_byte_size(reclaimed)}")
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
print("Nothing needed cleaning.")
|
|
148
|
+
print(retained_note)
|
|
149
|
+
|
|
150
|
+
|
|
49
151
|
def _progress(event: RuntimeEvent) -> None:
|
|
50
152
|
package = f" [{event.data['package']}]" if "package" in event.data else ""
|
|
51
153
|
print(f"lean-runtime: {event.kind}{package}: {event.message}", file=sys.stderr)
|
|
@@ -266,7 +368,8 @@ def parser() -> argparse.ArgumentParser:
|
|
|
266
368
|
inspect.add_argument("--explain", action="store_true", help="explain identity and reuse")
|
|
267
369
|
|
|
268
370
|
commands.add_parser("environments", help="list ready environments")
|
|
269
|
-
commands.add_parser("storage", help="show downloaded and built storage usage")
|
|
371
|
+
storage = commands.add_parser("storage", help="show downloaded and built storage usage")
|
|
372
|
+
storage.add_argument("--json", action="store_true")
|
|
270
373
|
commands.add_parser("doctor", help="check local prerequisites and environment storage")
|
|
271
374
|
|
|
272
375
|
verify = commands.add_parser("verify", help="verify a lock or published environment")
|
|
@@ -316,6 +419,7 @@ def parser() -> argparse.ArgumentParser:
|
|
|
316
419
|
gc.add_argument(
|
|
317
420
|
"--include-downloads", action="store_true", help="also clean unused downloaded files"
|
|
318
421
|
)
|
|
422
|
+
gc.add_argument("--json", action="store_true")
|
|
319
423
|
|
|
320
424
|
raw = commands.add_parser(
|
|
321
425
|
"check-file", help="check a file, discovering its local Lake project when possible"
|
|
@@ -640,7 +744,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
640
744
|
_json(list(runtime.list_environments()))
|
|
641
745
|
return 0
|
|
642
746
|
if args.command == "storage":
|
|
643
|
-
|
|
747
|
+
status = runtime.store_status()
|
|
748
|
+
if args.json:
|
|
749
|
+
_json(status.to_dict())
|
|
750
|
+
else:
|
|
751
|
+
_render_storage(status)
|
|
644
752
|
return 0
|
|
645
753
|
if args.command == "toolchain-slim":
|
|
646
754
|
manifest = runtime.toolchains.materialize_slim(args.toolchain)
|
|
@@ -734,22 +842,28 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
734
842
|
dry_run=not args.execute,
|
|
735
843
|
minimum_age_seconds=args.minimum_age_hours * 3600,
|
|
736
844
|
)
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
"downloaded_files": None,
|
|
740
|
-
}
|
|
741
|
-
if args.include_downloads:
|
|
742
|
-
gc_payload["downloaded_files"] = runtime.clean_downloads(
|
|
845
|
+
gc_downloads = (
|
|
846
|
+
runtime.clean_downloads(
|
|
743
847
|
dry_run=not args.execute,
|
|
744
848
|
minimum_age_seconds=args.minimum_age_hours * 3600,
|
|
745
|
-
).to_dict()
|
|
746
|
-
_json(
|
|
747
|
-
envelope(
|
|
748
|
-
"lean-runtime.cleanup/v1",
|
|
749
|
-
ok=True,
|
|
750
|
-
data=gc_payload,
|
|
751
849
|
)
|
|
850
|
+
if args.include_downloads
|
|
851
|
+
else None
|
|
752
852
|
)
|
|
853
|
+
if args.json:
|
|
854
|
+
gc_payload: dict[str, Any] = {
|
|
855
|
+
"environments": gc_report.to_dict(),
|
|
856
|
+
"downloaded_files": gc_downloads.to_dict() if gc_downloads else None,
|
|
857
|
+
}
|
|
858
|
+
_json(
|
|
859
|
+
envelope(
|
|
860
|
+
"lean-runtime.cleanup/v1",
|
|
861
|
+
ok=True,
|
|
862
|
+
data=gc_payload,
|
|
863
|
+
)
|
|
864
|
+
)
|
|
865
|
+
else:
|
|
866
|
+
_render_cleanup(gc_report, gc_downloads)
|
|
753
867
|
return 0
|
|
754
868
|
if args.command == "check":
|
|
755
869
|
if args.package_refs:
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Terminal rendering of structured runtime progress events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Literal, TextIO
|
|
9
|
+
|
|
10
|
+
from .events import RuntimeEvent
|
|
11
|
+
from .policies import format_byte_size
|
|
12
|
+
|
|
13
|
+
RenderMode = Literal["tty", "plain", "quiet"]
|
|
14
|
+
|
|
15
|
+
_BAR_WIDTH = 20
|
|
16
|
+
_TTY_REDRAW_SECONDS = 0.1
|
|
17
|
+
_PLAIN_CHECKPOINT_PERCENT = 25
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def select_mode(*, quiet: bool = False, stream: TextIO | None = None) -> RenderMode:
|
|
21
|
+
if quiet:
|
|
22
|
+
return "quiet"
|
|
23
|
+
stream = stream if stream is not None else sys.stderr
|
|
24
|
+
return "tty" if stream.isatty() else "plain"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Styler:
|
|
28
|
+
"""Minimal ANSI styling that degrades to plain text when disabled."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, enabled: bool) -> None:
|
|
31
|
+
self.enabled = enabled
|
|
32
|
+
|
|
33
|
+
def _wrap(self, code: str, text: str) -> str:
|
|
34
|
+
return f"\x1b[{code}m{text}\x1b[0m" if self.enabled else text
|
|
35
|
+
|
|
36
|
+
def bold(self, text: str) -> str:
|
|
37
|
+
return self._wrap("1", text)
|
|
38
|
+
|
|
39
|
+
def dim(self, text: str) -> str:
|
|
40
|
+
return self._wrap("2", text)
|
|
41
|
+
|
|
42
|
+
def red(self, text: str) -> str:
|
|
43
|
+
return self._wrap("31", text)
|
|
44
|
+
|
|
45
|
+
def green(self, text: str) -> str:
|
|
46
|
+
return self._wrap("32", text)
|
|
47
|
+
|
|
48
|
+
def yellow(self, text: str) -> str:
|
|
49
|
+
return self._wrap("33", text)
|
|
50
|
+
|
|
51
|
+
def cyan(self, text: str) -> str:
|
|
52
|
+
return self._wrap("36", text)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def styler_for(stream: TextIO | None = None) -> Styler:
|
|
56
|
+
"""Style only real terminals, honoring the NO_COLOR convention."""
|
|
57
|
+
stream = stream if stream is not None else sys.stderr
|
|
58
|
+
isatty = getattr(stream, "isatty", None)
|
|
59
|
+
enabled = bool(isatty and isatty()) and "NO_COLOR" not in os.environ
|
|
60
|
+
return Styler(enabled)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ConsoleRenderer:
|
|
64
|
+
"""Render runtime events as terse, single-purpose progress output.
|
|
65
|
+
|
|
66
|
+
Warm runs stay silent; cold acquisitions get one context line and one
|
|
67
|
+
aggregate progress line. Everything else is behind ``verbose``.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
stream: TextIO | None = None,
|
|
73
|
+
*,
|
|
74
|
+
mode: RenderMode | None = None,
|
|
75
|
+
verbose: bool = False,
|
|
76
|
+
color: bool | None = None,
|
|
77
|
+
clock: Any = time.monotonic,
|
|
78
|
+
) -> None:
|
|
79
|
+
self.stream = stream if stream is not None else sys.stderr
|
|
80
|
+
self.mode: RenderMode = mode if mode is not None else select_mode(stream=self.stream)
|
|
81
|
+
self.verbose = verbose
|
|
82
|
+
self.style = Styler(color) if color is not None else styler_for(self.stream)
|
|
83
|
+
self._clock = clock
|
|
84
|
+
self._download_total: int | None = None
|
|
85
|
+
self._layer_bytes: dict[str, int] = {}
|
|
86
|
+
self._line_length = 0
|
|
87
|
+
self._last_redraw: float | None = None
|
|
88
|
+
self._next_checkpoint = _PLAIN_CHECKPOINT_PERCENT
|
|
89
|
+
self._download_finished = False
|
|
90
|
+
|
|
91
|
+
def __call__(self, event: RuntimeEvent) -> None:
|
|
92
|
+
if self.mode == "quiet":
|
|
93
|
+
return
|
|
94
|
+
if self.verbose:
|
|
95
|
+
self._verbose_line(event)
|
|
96
|
+
return
|
|
97
|
+
handler = getattr(self, "_render_" + event.kind.replace(".", "_"), None)
|
|
98
|
+
if handler is not None:
|
|
99
|
+
handler(event)
|
|
100
|
+
|
|
101
|
+
def note(self, message: str) -> None:
|
|
102
|
+
"""Print one renderer-owned status line."""
|
|
103
|
+
if self.mode == "quiet":
|
|
104
|
+
return
|
|
105
|
+
self._print(message)
|
|
106
|
+
|
|
107
|
+
def close(self) -> None:
|
|
108
|
+
"""Terminate any in-place progress line before final output."""
|
|
109
|
+
self._end_progress_line()
|
|
110
|
+
|
|
111
|
+
# -- individual event renderings ------------------------------------
|
|
112
|
+
|
|
113
|
+
def _render_package_reference_started(self, event: RuntimeEvent) -> None:
|
|
114
|
+
reference = event.data.get("reference", "")
|
|
115
|
+
self._print(f"Resolving {reference}".rstrip())
|
|
116
|
+
|
|
117
|
+
def _render_discovery_candidate_started(self, event: RuntimeEvent) -> None:
|
|
118
|
+
candidate = event.data.get("candidate", "candidate")
|
|
119
|
+
toolchain = event.data.get("toolchain")
|
|
120
|
+
suffix = f" · {toolchain}" if toolchain else ""
|
|
121
|
+
self._print(f"Trying {candidate}{suffix}")
|
|
122
|
+
|
|
123
|
+
def _render_toolchain_install_started(self, event: RuntimeEvent) -> None:
|
|
124
|
+
toolchain = event.data.get("toolchain", "toolchain")
|
|
125
|
+
self._print(f"Installing Lean toolchain {toolchain} (one-time, can take minutes)")
|
|
126
|
+
|
|
127
|
+
def _render_acquisition_planned(self, event: RuntimeEvent) -> None:
|
|
128
|
+
download = event.data.get("download_bytes")
|
|
129
|
+
cached = event.data.get("cached_bytes")
|
|
130
|
+
if not isinstance(download, int) or download <= 0:
|
|
131
|
+
return
|
|
132
|
+
self._download_total = download
|
|
133
|
+
self._layer_bytes.clear()
|
|
134
|
+
self._next_checkpoint = _PLAIN_CHECKPOINT_PERCENT
|
|
135
|
+
self._download_finished = False
|
|
136
|
+
self._last_redraw = None
|
|
137
|
+
suffix = (
|
|
138
|
+
f" ({format_byte_size(cached)} already cached)"
|
|
139
|
+
if isinstance(cached, int) and cached > 0
|
|
140
|
+
else ""
|
|
141
|
+
)
|
|
142
|
+
self._print(f"Downloading environment: {format_byte_size(download)}{suffix}")
|
|
143
|
+
|
|
144
|
+
def _render_library_layer_progress(self, event: RuntimeEvent) -> None:
|
|
145
|
+
digest = str(event.data.get("digest", ""))
|
|
146
|
+
if event.current_bytes is None or self._download_total is None:
|
|
147
|
+
return
|
|
148
|
+
self._layer_bytes[digest] = event.current_bytes
|
|
149
|
+
done = min(sum(self._layer_bytes.values()), self._download_total)
|
|
150
|
+
if self.mode == "tty":
|
|
151
|
+
now = self._clock()
|
|
152
|
+
if (
|
|
153
|
+
self._last_redraw is not None
|
|
154
|
+
and done < self._download_total
|
|
155
|
+
and now - self._last_redraw < _TTY_REDRAW_SECONDS
|
|
156
|
+
):
|
|
157
|
+
return
|
|
158
|
+
self._last_redraw = now
|
|
159
|
+
self._draw_bar(done, self._download_total)
|
|
160
|
+
else:
|
|
161
|
+
percent = done * 100 // self._download_total
|
|
162
|
+
while self._next_checkpoint <= percent:
|
|
163
|
+
self._print(f"Downloaded {self._next_checkpoint}%")
|
|
164
|
+
self._next_checkpoint += _PLAIN_CHECKPOINT_PERCENT
|
|
165
|
+
|
|
166
|
+
def _render_library_layer_download_started(self, event: RuntimeEvent) -> None:
|
|
167
|
+
resumed = event.data.get("resumed_bytes")
|
|
168
|
+
if isinstance(resumed, int) and resumed > 0:
|
|
169
|
+
self._print(f"Resuming interrupted download from {format_byte_size(resumed)}")
|
|
170
|
+
|
|
171
|
+
def _render_library_layer_download_retry(self, event: RuntimeEvent) -> None:
|
|
172
|
+
self._print("Retrying download after integrity verification failed")
|
|
173
|
+
|
|
174
|
+
def _render_library_verified(self, event: RuntimeEvent) -> None:
|
|
175
|
+
if self._download_total is None or self._download_finished:
|
|
176
|
+
return
|
|
177
|
+
self._download_finished = True
|
|
178
|
+
if self.mode == "tty":
|
|
179
|
+
self._draw_bar(self._download_total, self._download_total)
|
|
180
|
+
self._end_progress_line()
|
|
181
|
+
self._print(self.style.green("Downloaded and verified environment"))
|
|
182
|
+
|
|
183
|
+
def _render_source_fetch_started(self, event: RuntimeEvent) -> None:
|
|
184
|
+
package = event.data.get("package", "sources")
|
|
185
|
+
self._print(f"Fetching {package} source")
|
|
186
|
+
|
|
187
|
+
def _render_artifact_hydration_started(self, event: RuntimeEvent) -> None:
|
|
188
|
+
package = event.data.get("package", "artifacts")
|
|
189
|
+
self._print(f"Hydrating build artifacts for {package}")
|
|
190
|
+
|
|
191
|
+
def _render_availability_fallback(self, event: RuntimeEvent) -> None:
|
|
192
|
+
library = event.data.get("library", "environment library")
|
|
193
|
+
reason = event.data.get("reason_code") or event.data.get("reason") or "unavailable"
|
|
194
|
+
self._print(
|
|
195
|
+
self.style.yellow(
|
|
196
|
+
f"WARNING: downloadable environment unavailable from {library} ({reason})"
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _render_environment_build_started(self, event: RuntimeEvent) -> None:
|
|
201
|
+
self._print(
|
|
202
|
+
"Building environment from source (large dependencies such as Mathlib can take "
|
|
203
|
+
"30+ minutes; pass --no-source-build to fail fast instead)"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _render_capability_required(self, event: RuntimeEvent) -> None:
|
|
207
|
+
self._print(event.message)
|
|
208
|
+
|
|
209
|
+
# -- low-level output -----------------------------------------------
|
|
210
|
+
|
|
211
|
+
def _verbose_line(self, event: RuntimeEvent) -> None:
|
|
212
|
+
parts = [f"{event.kind}: {event.message}"]
|
|
213
|
+
if event.current_bytes is not None and event.total_bytes is not None:
|
|
214
|
+
parts.append(
|
|
215
|
+
f"[{format_byte_size(event.current_bytes)}/{format_byte_size(event.total_bytes)}]"
|
|
216
|
+
)
|
|
217
|
+
if event.data:
|
|
218
|
+
details = " ".join(f"{key}={value}" for key, value in sorted(event.data.items()))
|
|
219
|
+
parts.append(f"({details})")
|
|
220
|
+
self._print(" ".join(parts))
|
|
221
|
+
|
|
222
|
+
def _draw_bar(self, done: int, total: int) -> None:
|
|
223
|
+
filled = _BAR_WIDTH * done // total if total else _BAR_WIDTH
|
|
224
|
+
bar = "█" * filled + "░" * (_BAR_WIDTH - filled)
|
|
225
|
+
percent = done * 100 // total if total else 100
|
|
226
|
+
sizes = f"{format_byte_size(done)}/{format_byte_size(total)}"
|
|
227
|
+
plain = f"Downloading [{bar}] {percent}% · {sizes}"
|
|
228
|
+
line = (
|
|
229
|
+
f"Downloading [{self.style.cyan(bar)}] "
|
|
230
|
+
f"{self.style.bold(f'{percent}%')} · {self.style.dim(sizes)}"
|
|
231
|
+
)
|
|
232
|
+
padding = " " * max(0, self._line_length - len(plain))
|
|
233
|
+
self.stream.write("\r" + line + padding)
|
|
234
|
+
self.stream.flush()
|
|
235
|
+
self._line_length = len(plain)
|
|
236
|
+
|
|
237
|
+
def _end_progress_line(self) -> None:
|
|
238
|
+
if self._line_length:
|
|
239
|
+
self.stream.write("\n")
|
|
240
|
+
self.stream.flush()
|
|
241
|
+
self._line_length = 0
|
|
242
|
+
|
|
243
|
+
def _print(self, message: str) -> None:
|
|
244
|
+
self._end_progress_line()
|
|
245
|
+
self.stream.write(message + "\n")
|
|
246
|
+
self.stream.flush()
|
|
@@ -78,6 +78,14 @@ class LeanRuntimeProbe:
|
|
|
78
78
|
cancel: threading.Event,
|
|
79
79
|
) -> AcquiredCandidate:
|
|
80
80
|
event_offset = len(self.events) if self.events is not None else 0
|
|
81
|
+
emitter = getattr(self.runtime, "events", None)
|
|
82
|
+
if emitter is not None:
|
|
83
|
+
emitter.emit(
|
|
84
|
+
"discovery.candidate_started",
|
|
85
|
+
f"Trying {candidate.entry.id}",
|
|
86
|
+
candidate=candidate.entry.id,
|
|
87
|
+
toolchain=candidate.entry.toolchain,
|
|
88
|
+
)
|
|
81
89
|
try:
|
|
82
90
|
environment = self.runtime.open_exact(
|
|
83
91
|
candidate.entry.lock,
|
|
@@ -64,6 +64,15 @@ class DownloadUnavailable(EnvironmentError):
|
|
|
64
64
|
"""A prebuilt cache had no usable artifact or could not be reached."""
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
class DownloadLimitExceeded(EnvironmentError):
|
|
68
|
+
"""An acquisition needs more bytes than the configured download limit.
|
|
69
|
+
|
|
70
|
+
Deliberately not a :class:`DownloadUnavailable`: exceeding a policy limit
|
|
71
|
+
must fail the operation instead of silently falling back to another
|
|
72
|
+
library or a source build.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
|
|
67
76
|
class MaterializationError(EnvironmentError):
|
|
68
77
|
"""Locked sources or build artifacts could not be materialized."""
|
|
69
78
|
|
|
@@ -16,9 +16,16 @@ class RuntimeEvent:
|
|
|
16
16
|
message: str
|
|
17
17
|
data: dict[str, Any] = field(default_factory=dict)
|
|
18
18
|
timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
19
|
+
phase: str | None = None
|
|
20
|
+
current_bytes: int | None = None
|
|
21
|
+
total_bytes: int | None = None
|
|
19
22
|
|
|
20
23
|
def to_dict(self) -> dict[str, Any]:
|
|
21
|
-
|
|
24
|
+
value = asdict(self)
|
|
25
|
+
for key in ("phase", "current_bytes", "total_bytes"):
|
|
26
|
+
if value[key] is None:
|
|
27
|
+
del value[key]
|
|
28
|
+
return value
|
|
22
29
|
|
|
23
30
|
|
|
24
31
|
EventCallback = Callable[[RuntimeEvent], None]
|
|
@@ -30,6 +37,24 @@ class EventEmitter:
|
|
|
30
37
|
def __init__(self, callback: EventCallback | None = None) -> None:
|
|
31
38
|
self.callback = callback
|
|
32
39
|
|
|
33
|
-
def emit(
|
|
40
|
+
def emit(
|
|
41
|
+
self,
|
|
42
|
+
kind: str,
|
|
43
|
+
message: str,
|
|
44
|
+
*,
|
|
45
|
+
phase: str | None = None,
|
|
46
|
+
current_bytes: int | None = None,
|
|
47
|
+
total_bytes: int | None = None,
|
|
48
|
+
**data: Any,
|
|
49
|
+
) -> None:
|
|
34
50
|
if self.callback is not None:
|
|
35
|
-
self.callback(
|
|
51
|
+
self.callback(
|
|
52
|
+
RuntimeEvent(
|
|
53
|
+
kind=kind,
|
|
54
|
+
message=message,
|
|
55
|
+
data=data,
|
|
56
|
+
phase=phase,
|
|
57
|
+
current_bytes=current_bytes,
|
|
58
|
+
total_bytes=total_bytes,
|
|
59
|
+
)
|
|
60
|
+
)
|