lean-runtime 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lean_runtime/errors.py ADDED
@@ -0,0 +1,63 @@
1
+ """Exceptions raised by :mod:`lean_runtime`."""
2
+
3
+
4
+ class LeanRuntimeError(RuntimeError):
5
+ """Base class for runtime infrastructure failures."""
6
+
7
+
8
+ class ToolchainError(LeanRuntimeError):
9
+ """A Lean toolchain could not be resolved or installed."""
10
+
11
+
12
+ class ProjectError(LeanRuntimeError):
13
+ """A Lean project is missing required configuration."""
14
+
15
+
16
+ class SpecificationError(LeanRuntimeError):
17
+ """An environment specification is invalid."""
18
+
19
+
20
+ class ResolutionError(LeanRuntimeError):
21
+ """Lake could not resolve an environment specification."""
22
+
23
+ def __init__(
24
+ self,
25
+ message: str,
26
+ *,
27
+ phase: str = "resolution",
28
+ command: tuple[str, ...] = (),
29
+ exit_code: int | None = None,
30
+ output: str = "",
31
+ ) -> None:
32
+ super().__init__(message)
33
+ self.phase = phase
34
+ self.command = command
35
+ self.exit_code = exit_code
36
+ self.output = output
37
+
38
+
39
+ class EnvironmentError(LeanRuntimeError):
40
+ """A content-addressed environment could not be opened or built."""
41
+
42
+
43
+ class MaterializationError(EnvironmentError):
44
+ """Locked sources or build artifacts could not be materialized."""
45
+
46
+ def __init__(
47
+ self,
48
+ message: str,
49
+ *,
50
+ phase: str,
51
+ command: tuple[str, ...] = (),
52
+ exit_code: int | None = None,
53
+ output: str = "",
54
+ ) -> None:
55
+ super().__init__(message)
56
+ self.phase = phase
57
+ self.command = command
58
+ self.exit_code = exit_code
59
+ self.output = output
60
+
61
+
62
+ class PolicyError(LeanRuntimeError):
63
+ """An execution policy cannot be enforced by the selected backend."""
lean_runtime/events.py ADDED
@@ -0,0 +1,35 @@
1
+ """Structured progress events emitted by long-running runtime operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import asdict, dataclass, field
7
+ from datetime import datetime, timezone
8
+ from typing import Any
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class RuntimeEvent:
13
+ """One observable lifecycle transition."""
14
+
15
+ kind: str
16
+ message: str
17
+ data: dict[str, Any] = field(default_factory=dict)
18
+ timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
19
+
20
+ def to_dict(self) -> dict[str, Any]:
21
+ return asdict(self)
22
+
23
+
24
+ EventCallback = Callable[[RuntimeEvent], None]
25
+
26
+
27
+ class EventEmitter:
28
+ """Null-safe adapter shared by runtime components."""
29
+
30
+ def __init__(self, callback: EventCallback | None = None) -> None:
31
+ self.callback = callback
32
+
33
+ def emit(self, kind: str, message: str, **data: Any) -> None:
34
+ if self.callback is not None:
35
+ self.callback(RuntimeEvent(kind=kind, message=message, data=data))
lean_runtime/health.py ADDED
@@ -0,0 +1,86 @@
1
+ """Local installation and cache health checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import subprocess
9
+ import tempfile
10
+ from dataclasses import asdict, dataclass
11
+ from typing import Literal
12
+
13
+ from .errors import ToolchainError
14
+ from .store import EnvironmentStore
15
+ from .toolchains import ToolchainManager
16
+
17
+ CheckStatus = Literal["pass", "warning", "fail"]
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class DoctorCheck:
22
+ name: str
23
+ status: CheckStatus
24
+ message: str
25
+
26
+ def to_dict(self) -> dict[str, str]:
27
+ return asdict(self)
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class DoctorReport:
32
+ checks: tuple[DoctorCheck, ...]
33
+
34
+ @property
35
+ def ok(self) -> bool:
36
+ return all(check.status != "fail" for check in self.checks)
37
+
38
+ def to_dict(self) -> dict[str, object]:
39
+ return {"ok": self.ok, "checks": [check.to_dict() for check in self.checks]}
40
+
41
+
42
+ def diagnose(toolchains: ToolchainManager, store: EnvironmentStore) -> DoctorReport:
43
+ """Inspect prerequisites without downloading or building anything."""
44
+ checks: list[DoctorCheck] = []
45
+ git = shutil.which("git")
46
+ if git is None:
47
+ checks.append(DoctorCheck("git", "fail", "Git is not available on PATH"))
48
+ else:
49
+ process = subprocess.run([git, "--version"], text=True, capture_output=True, check=False)
50
+ status: CheckStatus = "pass" if process.returncode == 0 else "fail"
51
+ checks.append(DoctorCheck("git", status, process.stdout.strip() or "Git failed"))
52
+
53
+ try:
54
+ with tempfile.NamedTemporaryFile(dir=store.home, prefix="doctor-", delete=True):
55
+ pass
56
+ checks.append(DoctorCheck("store", "pass", f"Store is writable: {store.home}"))
57
+ except OSError as exc:
58
+ checks.append(DoctorCheck("store", "fail", f"Store is not writable: {exc}"))
59
+
60
+ free = shutil.disk_usage(store.home).free
61
+ if free < 256 * 1024 * 1024:
62
+ checks.append(DoctorCheck("disk", "fail", f"Only {free // (1024**2)} MiB free"))
63
+ elif free < 2 * 1024 * 1024 * 1024:
64
+ checks.append(DoctorCheck("disk", "warning", f"Only {free // (1024**2)} MiB free"))
65
+ else:
66
+ checks.append(DoctorCheck("disk", "pass", f"{free // (1024**3)} GiB free"))
67
+
68
+ try:
69
+ elan = toolchains.elan_path(bootstrap=False)
70
+ except ToolchainError:
71
+ if os.name != "nt" and platform.system() in {"Darwin", "Linux"}:
72
+ checks.append(
73
+ DoctorCheck("elan", "warning", "Private Elan is not installed; it will bootstrap")
74
+ )
75
+ else:
76
+ checks.append(
77
+ DoctorCheck("elan", "fail", "Set LEAN_RUNTIME_ELAN to an Elan executable")
78
+ )
79
+ else:
80
+ checks.append(DoctorCheck("elan", "pass", f"Elan executable: {elan}"))
81
+
82
+ staging = tuple(store.environments.glob(".staging-*"))
83
+ status = "warning" if staging else "pass"
84
+ message = f"{len(staging)} incomplete staging directories" if staging else "No stale builds"
85
+ checks.append(DoctorCheck("staging", status, message))
86
+ return DoctorReport(tuple(checks))
lean_runtime/lake.py ADDED
@@ -0,0 +1,43 @@
1
+ """Synthetic Lake root generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from .specs import EnvironmentSpec
8
+
9
+ ROOT_PACKAGE = "lean_runtime_environment"
10
+ ROOT_MODULE = "LeanRuntimeEnvironment"
11
+
12
+
13
+ def _quote(value: str) -> str:
14
+ return json.dumps(value, ensure_ascii=False)
15
+
16
+
17
+ def generate_lakefile(spec: EnvironmentSpec) -> str:
18
+ lines = [
19
+ f"name = {_quote(ROOT_PACKAGE)}",
20
+ 'version = "0.0.0"',
21
+ 'defaultTargets = ["LeanRuntimeEnvironment"]',
22
+ "",
23
+ ]
24
+ for package in sorted(spec.packages, key=lambda item: item.name):
25
+ revision = package.rev.lower() if package.revision_kind == "commit" else package.rev
26
+ lines.extend(
27
+ [
28
+ "[[require]]",
29
+ f"name = {_quote(package.name)}",
30
+ f"git = {_quote(package.url)}",
31
+ f"rev = {_quote(revision)}",
32
+ ]
33
+ )
34
+ if package.subdir:
35
+ lines.append(f"subDir = {_quote(package.subdir)}")
36
+ lines.append("")
37
+ lines.extend(["[[lean_lib]]", f"name = {_quote(ROOT_MODULE)}", ""])
38
+ return "\n".join(lines)
39
+
40
+
41
+ def generate_root_module(spec: EnvironmentSpec) -> str:
42
+ imports = [f"import {package.module}" for package in spec.packages]
43
+ return "\n".join([*imports, "", "/- Generated by Lean Runtime. -/", ""])
@@ -0,0 +1,192 @@
1
+ """Portable environment locks produced by Lake-backed resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .errors import EnvironmentError
12
+ from .serialization import sha256_id, write_json_atomic
13
+
14
+ LOCK_SCHEMA = "lean-runtime-environment-lock/1"
15
+ _NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_'-]*")
16
+ _MODULE = re.compile(r"[A-Za-z_][A-Za-z0-9_']*(?:\.[A-Za-z_][A-Za-z0-9_']*)*")
17
+ _COMMIT = re.compile(r"[0-9a-fA-F]{40}")
18
+ _SOURCE_ID = re.compile(r"source_[0-9a-f]{64}")
19
+ _SPEC_ID = re.compile(r"spec_[0-9a-f]{64}")
20
+
21
+
22
+ def _optional_string(value: Any, field_name: str) -> str | None:
23
+ if value is None:
24
+ return None
25
+ if not isinstance(value, str):
26
+ raise EnvironmentError(f"lock field {field_name!r} must be a string or null")
27
+ return value
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class LockedPackage:
32
+ name: str
33
+ url: str
34
+ revision: str
35
+ source_id: str
36
+ tree_hash: str
37
+ requested_revision: str | None = None
38
+ inherited: bool = False
39
+ subdir: str | None = None
40
+ root_module: str | None = None
41
+ artifact_command: tuple[str, ...] = field(default_factory=tuple)
42
+
43
+ def __post_init__(self) -> None:
44
+ if _NAME.fullmatch(self.name) is None:
45
+ raise EnvironmentError(f"invalid locked package name: {self.name!r}")
46
+ if not self.url or "\n" in self.url or "\r" in self.url:
47
+ raise EnvironmentError(f"invalid locked Git URL for package {self.name!r}")
48
+ if _COMMIT.fullmatch(self.revision) is None:
49
+ raise EnvironmentError(f"invalid locked revision for package {self.name!r}")
50
+ if _COMMIT.fullmatch(self.tree_hash) is None:
51
+ raise EnvironmentError(f"invalid locked tree hash for package {self.name!r}")
52
+ if _SOURCE_ID.fullmatch(self.source_id) is None:
53
+ raise EnvironmentError(f"invalid source identity for package {self.name!r}")
54
+ if self.requested_revision is not None and not self.requested_revision:
55
+ raise EnvironmentError(f"empty requested revision for package {self.name!r}")
56
+ if self.root_module is not None and _MODULE.fullmatch(self.root_module) is None:
57
+ raise EnvironmentError(f"invalid root module for package {self.name!r}")
58
+ if self.subdir is not None:
59
+ subdir = Path(self.subdir)
60
+ if not self.subdir or subdir.is_absolute() or ".." in subdir.parts:
61
+ raise EnvironmentError(f"unsafe package subdir in lock: {self.subdir!r}")
62
+ if any(not item or "\x00" in item for item in self.artifact_command):
63
+ raise EnvironmentError(f"invalid artifact command for package {self.name!r}")
64
+
65
+ def to_dict(self) -> dict[str, Any]:
66
+ return {
67
+ "name": self.name,
68
+ "source": "git",
69
+ "url": self.url,
70
+ "requested_revision": self.requested_revision,
71
+ "revision": self.revision,
72
+ "tree_hash": self.tree_hash,
73
+ "source_id": self.source_id,
74
+ "inherited": self.inherited,
75
+ "subdir": self.subdir,
76
+ "root_module": self.root_module,
77
+ "artifact_command": list(self.artifact_command),
78
+ }
79
+
80
+ @classmethod
81
+ def from_dict(cls, value: dict[str, Any]) -> LockedPackage:
82
+ if value.get("source") != "git":
83
+ raise EnvironmentError("lock contains an unsupported package source")
84
+ command = value.get("artifact_command", [])
85
+ if not isinstance(command, list) or not all(isinstance(item, str) for item in command):
86
+ raise EnvironmentError("locked artifact_command must be an array of strings")
87
+ inherited = value.get("inherited", False)
88
+ if not isinstance(inherited, bool):
89
+ raise EnvironmentError("locked package inherited must be Boolean")
90
+ required = ("name", "url", "revision", "tree_hash", "source_id")
91
+ if any(not isinstance(value.get(key), str) for key in required):
92
+ raise EnvironmentError("locked package is missing a required string field")
93
+ return cls(
94
+ name=value["name"],
95
+ url=value["url"],
96
+ requested_revision=_optional_string(
97
+ value.get("requested_revision"), "requested_revision"
98
+ ),
99
+ revision=value["revision"],
100
+ tree_hash=value["tree_hash"],
101
+ source_id=value["source_id"],
102
+ inherited=inherited,
103
+ subdir=_optional_string(value.get("subdir"), "subdir"),
104
+ root_module=_optional_string(value.get("root_module"), "root_module"),
105
+ artifact_command=tuple(command),
106
+ )
107
+
108
+
109
+ @dataclass(frozen=True, slots=True)
110
+ class EnvironmentLock:
111
+ """A canonical, platform-independent resolved dependency graph."""
112
+
113
+ toolchain: str
114
+ spec_digest: str
115
+ root_lakefile: str
116
+ root_module: str
117
+ manifest: dict[str, Any]
118
+ packages: tuple[LockedPackage, ...]
119
+
120
+ def __post_init__(self) -> None:
121
+ if not self.toolchain or "\x00" in self.toolchain:
122
+ raise EnvironmentError("lock contains an invalid toolchain")
123
+ if _SPEC_ID.fullmatch(self.spec_digest) is None:
124
+ raise EnvironmentError("lock contains an invalid specification digest")
125
+ if not self.root_lakefile or "\x00" in self.root_lakefile:
126
+ raise EnvironmentError("lock contains an invalid root Lake configuration")
127
+ if "\x00" in self.root_module:
128
+ raise EnvironmentError("lock contains invalid root Lean source")
129
+ names = [package.name for package in self.packages]
130
+ if len(names) != len(set(names)):
131
+ raise EnvironmentError("lock contains duplicate package names")
132
+
133
+ def identity_payload(self) -> dict[str, Any]:
134
+ return {
135
+ "schema": LOCK_SCHEMA,
136
+ "toolchain": self.toolchain,
137
+ "spec_digest": self.spec_digest,
138
+ "root_lakefile": self.root_lakefile,
139
+ "root_module": self.root_module,
140
+ "manifest": self.manifest,
141
+ "packages": [
142
+ package.to_dict() for package in sorted(self.packages, key=lambda item: item.name)
143
+ ],
144
+ }
145
+
146
+ @property
147
+ def lock_id(self) -> str:
148
+ return sha256_id("lock", self.identity_payload())
149
+
150
+ def to_dict(self) -> dict[str, Any]:
151
+ return {"lock_id": self.lock_id, **self.identity_payload()}
152
+
153
+ def write(self, path: str | Path) -> None:
154
+ write_json_atomic(Path(path), self.to_dict())
155
+
156
+ @classmethod
157
+ def from_dict(cls, value: dict[str, Any]) -> EnvironmentLock:
158
+ if value.get("schema") != LOCK_SCHEMA:
159
+ raise EnvironmentError(f"unsupported lock schema: {value.get('schema')!r}")
160
+ required_strings = ("toolchain", "spec_digest", "root_lakefile", "root_module")
161
+ if any(not isinstance(value.get(key), str) for key in required_strings):
162
+ raise EnvironmentError("environment lock is missing a required string field")
163
+ manifest = value.get("manifest")
164
+ packages = value.get("packages")
165
+ if not isinstance(manifest, dict):
166
+ raise EnvironmentError("environment lock manifest must be an object")
167
+ if not isinstance(packages, list) or not all(isinstance(item, dict) for item in packages):
168
+ raise EnvironmentError("environment lock packages must be an array of objects")
169
+ lock = cls(
170
+ toolchain=value["toolchain"],
171
+ spec_digest=value["spec_digest"],
172
+ root_lakefile=value["root_lakefile"],
173
+ root_module=value["root_module"],
174
+ manifest=manifest,
175
+ packages=tuple(LockedPackage.from_dict(item) for item in packages),
176
+ )
177
+ recorded = value.get("lock_id")
178
+ if recorded is not None and recorded != lock.lock_id:
179
+ raise EnvironmentError(
180
+ f"lock identity mismatch: recorded {recorded!r}, computed {lock.lock_id!r}"
181
+ )
182
+ return lock
183
+
184
+ @classmethod
185
+ def load(cls, path: str | Path) -> EnvironmentLock:
186
+ try:
187
+ value = json.loads(Path(path).read_text(encoding="utf-8"))
188
+ except (OSError, json.JSONDecodeError) as exc:
189
+ raise EnvironmentError(f"could not read environment lock: {path}") from exc
190
+ if not isinstance(value, dict):
191
+ raise EnvironmentError("environment lock must contain an object")
192
+ return cls.from_dict(value)
@@ -0,0 +1,82 @@
1
+ """Small cross-process file locks used for atomic store publication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from pathlib import Path
8
+ from types import TracebackType
9
+ from typing import BinaryIO
10
+
11
+ from .errors import EnvironmentError
12
+
13
+
14
+ class FileLock:
15
+ def __init__(self, path: Path, timeout: float = 300) -> None:
16
+ self.path = path
17
+ self.timeout = timeout
18
+ self._handle: BinaryIO | None = None
19
+
20
+ def __enter__(self) -> FileLock:
21
+ self.path.parent.mkdir(parents=True, exist_ok=True)
22
+ handle = self.path.open("a+b")
23
+ started = time.monotonic()
24
+ while True:
25
+ try:
26
+ if os.name == "nt":
27
+ import msvcrt
28
+
29
+ handle.seek(0)
30
+ if handle.tell() == 0:
31
+ handle.write(b"0")
32
+ handle.flush()
33
+ handle.seek(0)
34
+ getattr(msvcrt, "locking")( # noqa: B009
35
+ handle.fileno(),
36
+ getattr(msvcrt, "LK_NBLCK"), # noqa: B009
37
+ 1, # noqa: B009
38
+ )
39
+ else:
40
+ import fcntl
41
+
42
+ getattr(fcntl, "flock")( # noqa: B009
43
+ handle.fileno(),
44
+ getattr(fcntl, "LOCK_EX") | getattr(fcntl, "LOCK_NB"), # noqa: B009
45
+ )
46
+ self._handle = handle
47
+ return self
48
+ except OSError as error:
49
+ if time.monotonic() - started >= self.timeout:
50
+ handle.close()
51
+ raise EnvironmentError(
52
+ f"timed out waiting for store lock: {self.path}"
53
+ ) from error
54
+ time.sleep(0.05)
55
+
56
+ def __exit__(
57
+ self,
58
+ exc_type: type[BaseException] | None,
59
+ exc: BaseException | None,
60
+ traceback: TracebackType | None,
61
+ ) -> None:
62
+ handle = self._handle
63
+ if handle is None:
64
+ return
65
+ if os.name == "nt":
66
+ import msvcrt
67
+
68
+ handle.seek(0)
69
+ getattr(msvcrt, "locking")( # noqa: B009
70
+ handle.fileno(),
71
+ getattr(msvcrt, "LK_UNLCK"), # noqa: B009
72
+ 1, # noqa: B009
73
+ )
74
+ else:
75
+ import fcntl
76
+
77
+ getattr(fcntl, "flock")( # noqa: B009
78
+ handle.fileno(),
79
+ getattr(fcntl, "LOCK_UN"), # noqa: B009
80
+ )
81
+ handle.close()
82
+ self._handle = None
lean_runtime/models.py ADDED
@@ -0,0 +1,78 @@
1
+ """Stable result models returned by the runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass, field
6
+ from typing import Any, Literal
7
+
8
+ Severity = Literal["error", "warning", "information", "unknown"]
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class Diagnostic:
13
+ """One compiler diagnostic parsed from Lean's textual output."""
14
+
15
+ severity: Severity
16
+ message: str
17
+ file: str | None = None
18
+ line: int | None = None
19
+ column: int | None = None
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class PackageProvenance:
24
+ name: str
25
+ url: str
26
+ revision: str
27
+ tree_hash: str
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class ExecutionProvenance:
32
+ environment_id: str | None
33
+ execution_id: str
34
+ request_digest: str
35
+ lock_id: str | None
36
+ toolchain: str
37
+ packages: tuple[PackageProvenance, ...]
38
+ platform: dict[str, str]
39
+ backend: str
40
+ requested_policy: dict[str, Any]
41
+ enforced_policy_fields: tuple[str, ...]
42
+ source_digest: str
43
+ started_at: str
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class ExecutionResult:
48
+ """The reproducible outcome of one Lean or Lake invocation."""
49
+
50
+ ok: bool
51
+ exit_code: int
52
+ toolchain: str
53
+ command: tuple[str, ...]
54
+ cwd: str
55
+ stdout: str
56
+ stderr: str
57
+ elapsed_seconds: float
58
+ timed_out: bool = False
59
+ cancelled: bool = False
60
+ output_truncated: bool = False
61
+ diagnostics: tuple[Diagnostic, ...] = field(default_factory=tuple)
62
+ provenance: ExecutionProvenance | None = None
63
+
64
+ @property
65
+ def environment_id(self) -> str | None:
66
+ return self.provenance.environment_id if self.provenance else None
67
+
68
+ @property
69
+ def execution_id(self) -> str | None:
70
+ return self.provenance.execution_id if self.provenance else None
71
+
72
+ @property
73
+ def lock_id(self) -> str | None:
74
+ return self.provenance.lock_id if self.provenance else None
75
+
76
+ def to_dict(self) -> dict[str, Any]:
77
+ """Return a JSON-serializable representation."""
78
+ return asdict(self)
@@ -0,0 +1,36 @@
1
+ """Backend-independent execution policy requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ from typing import Any, Literal
7
+
8
+ NetworkPolicy = Literal["inherit", "disabled"]
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class ExecutionPolicy:
13
+ timeout_seconds: float = 120
14
+ max_output_bytes: int = 1_000_000
15
+ memory_mb: int | None = None
16
+ cpu_seconds: int | None = None
17
+ network: NetworkPolicy = "inherit"
18
+
19
+ def __post_init__(self) -> None:
20
+ if self.timeout_seconds <= 0:
21
+ raise ValueError("timeout_seconds must be positive")
22
+ if self.max_output_bytes < 0:
23
+ raise ValueError("max_output_bytes must be nonnegative")
24
+ if self.memory_mb is not None and self.memory_mb <= 0:
25
+ raise ValueError("memory_mb must be positive")
26
+ if self.cpu_seconds is not None and self.cpu_seconds <= 0:
27
+ raise ValueError("cpu_seconds must be positive")
28
+ if self.network not in {"inherit", "disabled"}:
29
+ raise ValueError(f"unsupported network policy: {self.network!r}")
30
+
31
+ @classmethod
32
+ def trusted_local(cls, **kwargs: Any) -> ExecutionPolicy:
33
+ return cls(**kwargs)
34
+
35
+ def to_dict(self) -> dict[str, Any]:
36
+ return asdict(self)
lean_runtime/py.typed ADDED
@@ -0,0 +1 @@
1
+