telchines 1.0.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.
- telchines/__init__.py +5 -0
- telchines/__main__.py +7 -0
- telchines/adapters/__init__.py +1 -0
- telchines/adapters/base.py +130 -0
- telchines/adapters/open_tools.py +187 -0
- telchines/adapters/parsing.py +58 -0
- telchines/adapters/registry.py +27 -0
- telchines/cli.py +315 -0
- telchines/config.py +230 -0
- telchines/errors.py +21 -0
- telchines/eval.py +483 -0
- telchines/models.py +357 -0
- telchines/operations.py +405 -0
- telchines/providers.py +1210 -0
- telchines/retrieval.py +405 -0
- telchines/run_store.py +139 -0
- telchines/shell.py +1022 -0
- telchines/utils.py +87 -0
- telchines/waveforms.py +187 -0
- telchines/workflows/__init__.py +1 -0
- telchines/workflows/coverage.py +483 -0
- telchines/workflows/gen_cocotb.py +240 -0
- telchines/workflows/gen_sva.py +176 -0
- telchines/workflows/repair.py +183 -0
- telchines/workflows/triage.py +298 -0
- telchines-1.0.0.dist-info/METADATA +312 -0
- telchines-1.0.0.dist-info/RECORD +31 -0
- telchines-1.0.0.dist-info/WHEEL +5 -0
- telchines-1.0.0.dist-info/entry_points.txt +3 -0
- telchines-1.0.0.dist-info/licenses/LICENSE +21 -0
- telchines-1.0.0.dist-info/top_level.txt +1 -0
telchines/__init__.py
ADDED
telchines/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tool adapters for supported verification tools."""
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from telchines.adapters.parsing import parse_common_output
|
|
10
|
+
from telchines.errors import AdapterExecutionError
|
|
11
|
+
from telchines.models import AdapterDescriptor, Observation, ToolReference
|
|
12
|
+
from telchines.utils import ensure_directory, utc_now
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class AdapterExecution:
|
|
17
|
+
command: list[str]
|
|
18
|
+
cwd: str
|
|
19
|
+
exit_code: int
|
|
20
|
+
stdout: str
|
|
21
|
+
stderr: str
|
|
22
|
+
log_path: str
|
|
23
|
+
started_at: str
|
|
24
|
+
finished_at: str
|
|
25
|
+
observations: list[Observation]
|
|
26
|
+
summary: str
|
|
27
|
+
artifacts: dict[str, str] = field(default_factory=dict)
|
|
28
|
+
result: dict[str, Any] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ToolAdapter:
|
|
32
|
+
name = "base"
|
|
33
|
+
kind = "tool"
|
|
34
|
+
category = "tool"
|
|
35
|
+
validation_mode = "compile_only"
|
|
36
|
+
binary_names: tuple[str, ...] = ()
|
|
37
|
+
required_binaries: tuple[str, ...] = ()
|
|
38
|
+
supported_workflows: tuple[str, ...] = ()
|
|
39
|
+
artifact_types: tuple[str, ...] = ("log",)
|
|
40
|
+
|
|
41
|
+
def is_available(self) -> bool:
|
|
42
|
+
required = self.required_binaries or self.binary_names
|
|
43
|
+
if not required:
|
|
44
|
+
return True
|
|
45
|
+
return all(shutil.which(binary) for binary in required)
|
|
46
|
+
|
|
47
|
+
def version(self) -> str:
|
|
48
|
+
return "unknown"
|
|
49
|
+
|
|
50
|
+
def build_command(self, project_root: Path, files: list[str], extra_args: list[str] | None = None) -> list[str]:
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
53
|
+
def parse_output(self, run_id: str, text: str) -> list[Observation]:
|
|
54
|
+
return parse_common_output(run_id, text)
|
|
55
|
+
|
|
56
|
+
def parse_result(self, project_root: Path, files: list[str], stdout: str, stderr: str, combined: str) -> dict[str, Any]:
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
def collect_artifacts(
|
|
60
|
+
self,
|
|
61
|
+
project_root: Path,
|
|
62
|
+
files: list[str],
|
|
63
|
+
artifacts_dir: Path,
|
|
64
|
+
run_id: str,
|
|
65
|
+
stdout: str,
|
|
66
|
+
stderr: str,
|
|
67
|
+
combined: str,
|
|
68
|
+
) -> dict[str, str]:
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
def describe(self, *, enabled: bool = False) -> AdapterDescriptor:
|
|
72
|
+
return AdapterDescriptor(
|
|
73
|
+
name=self.name,
|
|
74
|
+
kind=self.kind,
|
|
75
|
+
category=self.category,
|
|
76
|
+
validation_mode=self.validation_mode,
|
|
77
|
+
binary_names=list(self.binary_names),
|
|
78
|
+
required_binaries=list(self.required_binaries or self.binary_names),
|
|
79
|
+
supported_workflows=list(self.supported_workflows),
|
|
80
|
+
artifact_types=list(self.artifact_types),
|
|
81
|
+
available=self.is_available(),
|
|
82
|
+
enabled=enabled,
|
|
83
|
+
version=self.version(),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def run(self, run_id: str, project_root: Path, files: list[str], artifacts_dir: Path, extra_args: list[str] | None = None) -> AdapterExecution:
|
|
87
|
+
command = self.build_command(project_root, files, extra_args)
|
|
88
|
+
if not files:
|
|
89
|
+
raise AdapterExecutionError(f"{self.name} requires at least one input file")
|
|
90
|
+
if not self.is_available():
|
|
91
|
+
binaries = ", ".join(self.binary_names) or self.name
|
|
92
|
+
raise AdapterExecutionError(f"{self.name} is not available on PATH; expected one of: {binaries}")
|
|
93
|
+
started_at = utc_now()
|
|
94
|
+
try:
|
|
95
|
+
process = subprocess.run(command, cwd=project_root, capture_output=True, text=True, check=False)
|
|
96
|
+
except OSError as exc:
|
|
97
|
+
raise AdapterExecutionError(f"failed to execute {self.name}: {exc}") from exc
|
|
98
|
+
finished_at = utc_now()
|
|
99
|
+
ensure_directory(artifacts_dir)
|
|
100
|
+
log_path = artifacts_dir / f"{run_id}.log"
|
|
101
|
+
combined = process.stdout + process.stderr
|
|
102
|
+
log_path.write_text(combined, encoding="utf-8")
|
|
103
|
+
observations = self.parse_output(run_id, combined)
|
|
104
|
+
result = self.parse_result(project_root, files, process.stdout, process.stderr, combined)
|
|
105
|
+
artifacts = {"log_path": str(log_path)}
|
|
106
|
+
artifacts.update(self.collect_artifacts(project_root, files, artifacts_dir, run_id, process.stdout, process.stderr, combined))
|
|
107
|
+
summary = f"{self.name} exited with code {process.returncode}"
|
|
108
|
+
normalized_status = str(result.get("status", "")).strip()
|
|
109
|
+
if normalized_status:
|
|
110
|
+
summary = f"{summary}; status: {normalized_status}"
|
|
111
|
+
if observations:
|
|
112
|
+
summary = f"{summary}; first observation: {observations[0].signature}"
|
|
113
|
+
return AdapterExecution(
|
|
114
|
+
command=command,
|
|
115
|
+
cwd=str(project_root),
|
|
116
|
+
exit_code=process.returncode,
|
|
117
|
+
stdout=process.stdout,
|
|
118
|
+
stderr=process.stderr,
|
|
119
|
+
log_path=str(log_path),
|
|
120
|
+
started_at=started_at,
|
|
121
|
+
finished_at=finished_at,
|
|
122
|
+
observations=observations,
|
|
123
|
+
summary=summary,
|
|
124
|
+
artifacts=artifacts,
|
|
125
|
+
result=result,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def tool_reference(self) -> ToolReference:
|
|
130
|
+
return ToolReference(kind=self.kind, name=self.name, version=self.version())
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from telchines.adapters.base import AdapterExecution, ToolAdapter
|
|
9
|
+
from telchines.errors import AdapterExecutionError
|
|
10
|
+
from telchines.utils import ensure_directory, utc_now
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VerilatorAdapter(ToolAdapter):
|
|
14
|
+
name = "verilator"
|
|
15
|
+
kind = "simulator"
|
|
16
|
+
category = "simulation"
|
|
17
|
+
binary_names = ("verilator",)
|
|
18
|
+
supported_workflows = ("repair_validation",)
|
|
19
|
+
artifact_types = ("log", "stdout", "stderr")
|
|
20
|
+
|
|
21
|
+
def build_command(self, project_root: Path, files: list[str], extra_args: list[str] | None = None) -> list[str]:
|
|
22
|
+
return ["verilator", "--lint-only", *(extra_args or []), *files]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IcarusAdapter(ToolAdapter):
|
|
26
|
+
name = "iverilog"
|
|
27
|
+
kind = "simulator"
|
|
28
|
+
category = "simulation"
|
|
29
|
+
validation_mode = "compile_and_run"
|
|
30
|
+
binary_names = ("iverilog", "vvp")
|
|
31
|
+
required_binaries = ("iverilog", "vvp")
|
|
32
|
+
supported_workflows = ("repair_validation",)
|
|
33
|
+
artifact_types = ("log", "stdout", "stderr", "executable")
|
|
34
|
+
|
|
35
|
+
def build_command(self, project_root: Path, files: list[str], extra_args: list[str] | None = None) -> list[str]:
|
|
36
|
+
return ["iverilog", "-g2012", *(extra_args or []), *files]
|
|
37
|
+
|
|
38
|
+
def run(self, run_id: str, project_root: Path, files: list[str], artifacts_dir: Path, extra_args: list[str] | None = None) -> AdapterExecution:
|
|
39
|
+
if not files:
|
|
40
|
+
raise AdapterExecutionError(f"{self.name} requires at least one input file")
|
|
41
|
+
if not self.is_available():
|
|
42
|
+
binaries = ", ".join(self.required_binaries or self.binary_names) or self.name
|
|
43
|
+
raise AdapterExecutionError(f"{self.name} is not available on PATH; expected: {binaries}")
|
|
44
|
+
|
|
45
|
+
ensure_directory(artifacts_dir)
|
|
46
|
+
executable_path = artifacts_dir / f"{run_id}.out"
|
|
47
|
+
compile_command = ["iverilog", "-g2012", "-o", str(executable_path), *(extra_args or []), *files]
|
|
48
|
+
compile_started_at = utc_now()
|
|
49
|
+
try:
|
|
50
|
+
compile_process = subprocess.run(compile_command, cwd=project_root, capture_output=True, text=True, check=False)
|
|
51
|
+
except OSError as exc:
|
|
52
|
+
raise AdapterExecutionError(f"failed to execute {self.name} compile step: {exc}") from exc
|
|
53
|
+
|
|
54
|
+
run_command = ["vvp", str(executable_path)]
|
|
55
|
+
run_stdout = ""
|
|
56
|
+
run_stderr = ""
|
|
57
|
+
run_exit_code: int | None = None
|
|
58
|
+
if compile_process.returncode == 0:
|
|
59
|
+
try:
|
|
60
|
+
run_process = subprocess.run(run_command, cwd=project_root, capture_output=True, text=True, check=False)
|
|
61
|
+
except OSError as exc:
|
|
62
|
+
raise AdapterExecutionError(f"failed to execute {self.name} run step: {exc}") from exc
|
|
63
|
+
run_stdout = run_process.stdout
|
|
64
|
+
run_stderr = run_process.stderr
|
|
65
|
+
run_exit_code = run_process.returncode
|
|
66
|
+
|
|
67
|
+
finished_at = utc_now()
|
|
68
|
+
combined = compile_process.stdout + compile_process.stderr + run_stdout + run_stderr
|
|
69
|
+
log_path = artifacts_dir / f"{run_id}.log"
|
|
70
|
+
log_path.write_text(combined, encoding="utf-8")
|
|
71
|
+
observations = self.parse_output(run_id, combined)
|
|
72
|
+
overall_exit_code = compile_process.returncode if compile_process.returncode != 0 else (run_exit_code or 0)
|
|
73
|
+
result = {
|
|
74
|
+
"status": "passed" if overall_exit_code == 0 else "failed",
|
|
75
|
+
"validation_mode": self.validation_mode,
|
|
76
|
+
"compile_command": compile_command,
|
|
77
|
+
"run_command": run_command if compile_process.returncode == 0 else [],
|
|
78
|
+
"compile_exit_code": compile_process.returncode,
|
|
79
|
+
"run_exit_code": run_exit_code,
|
|
80
|
+
"artifact_path": str(executable_path),
|
|
81
|
+
}
|
|
82
|
+
summary = f"{self.name} compile+run exited with code {overall_exit_code}"
|
|
83
|
+
if observations:
|
|
84
|
+
summary = f"{summary}; first observation: {observations[0].signature}"
|
|
85
|
+
return AdapterExecution(
|
|
86
|
+
command=compile_command,
|
|
87
|
+
cwd=str(project_root),
|
|
88
|
+
exit_code=overall_exit_code,
|
|
89
|
+
stdout=compile_process.stdout + run_stdout,
|
|
90
|
+
stderr=compile_process.stderr + run_stderr,
|
|
91
|
+
log_path=str(log_path),
|
|
92
|
+
started_at=compile_started_at,
|
|
93
|
+
finished_at=finished_at,
|
|
94
|
+
observations=observations,
|
|
95
|
+
summary=summary,
|
|
96
|
+
artifacts={"log_path": str(log_path), "compiled_executable": str(executable_path)},
|
|
97
|
+
result=result,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SlangAdapter(ToolAdapter):
|
|
102
|
+
name = "slang"
|
|
103
|
+
kind = "simulator"
|
|
104
|
+
category = "simulation"
|
|
105
|
+
binary_names = ("slang",)
|
|
106
|
+
supported_workflows = ("repair_validation", "generation_validation")
|
|
107
|
+
artifact_types = ("log", "stdout", "stderr")
|
|
108
|
+
|
|
109
|
+
def build_command(self, project_root: Path, files: list[str], extra_args: list[str] | None = None) -> list[str]:
|
|
110
|
+
return ["slang", "--lint-only", *(extra_args or []), *files]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class VeribleAdapter(ToolAdapter):
|
|
114
|
+
name = "verible"
|
|
115
|
+
kind = "linter"
|
|
116
|
+
category = "lint"
|
|
117
|
+
binary_names = ("verible-verilog-lint",)
|
|
118
|
+
supported_workflows = ("repair_validation",)
|
|
119
|
+
artifact_types = ("log", "stdout", "stderr")
|
|
120
|
+
|
|
121
|
+
def build_command(self, project_root: Path, files: list[str], extra_args: list[str] | None = None) -> list[str]:
|
|
122
|
+
return ["verible-verilog-lint", *(extra_args or []), *files]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class SymbiYosysAdapter(ToolAdapter):
|
|
126
|
+
name = "symbiyosys"
|
|
127
|
+
kind = "formal"
|
|
128
|
+
category = "formal"
|
|
129
|
+
binary_names = ("sby",)
|
|
130
|
+
supported_workflows = ("formal_validation",)
|
|
131
|
+
artifact_types = ("log", "report", "counterexample")
|
|
132
|
+
|
|
133
|
+
def build_command(self, project_root: Path, files: list[str], extra_args: list[str] | None = None) -> list[str]:
|
|
134
|
+
return ["sby", *(extra_args or []), *files]
|
|
135
|
+
|
|
136
|
+
def parse_result(self, project_root: Path, files: list[str], stdout: str, stderr: str, combined: str) -> dict[str, Any]:
|
|
137
|
+
return _parse_symbiyosys_result(project_root, combined)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _parse_symbiyosys_result(project_root: Path, combined: str) -> dict[str, Any]:
|
|
141
|
+
lowered = combined.lower()
|
|
142
|
+
status = "unknown"
|
|
143
|
+
if re.search(r"\b(status|summary)\s*:\s*passed\b", lowered):
|
|
144
|
+
status = "passed"
|
|
145
|
+
elif re.search(r"\b(status|summary)\s*:\s*failed\b", lowered):
|
|
146
|
+
status = "failed"
|
|
147
|
+
elif "assert failed" in lowered or "counterexample" in lowered:
|
|
148
|
+
status = "failed"
|
|
149
|
+
|
|
150
|
+
property_ids = sorted(
|
|
151
|
+
{
|
|
152
|
+
match.group("name")
|
|
153
|
+
for match in re.finditer(
|
|
154
|
+
r"(?P<name>[A-Za-z_][A-Za-z0-9_$]*)\s+(?:proved|failed|covered|unreached)",
|
|
155
|
+
combined,
|
|
156
|
+
flags=re.IGNORECASE,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
referenced_artifacts = _existing_artifacts(project_root, combined)
|
|
161
|
+
counterexample_paths = [path for path in referenced_artifacts if path.endswith((".vcd", ".fst"))]
|
|
162
|
+
report_paths = [path for path in referenced_artifacts if path.endswith((".json", ".txt", ".log"))]
|
|
163
|
+
return {
|
|
164
|
+
"status": status,
|
|
165
|
+
"property_ids": property_ids,
|
|
166
|
+
"counterexample_paths": counterexample_paths,
|
|
167
|
+
"report_paths": report_paths,
|
|
168
|
+
"referenced_artifacts": referenced_artifacts,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _existing_artifacts(project_root: Path, combined: str) -> list[str]:
|
|
173
|
+
candidates = re.findall(r"([A-Za-z0-9_./\\\\-]+\.(?:vcd|fst|json|txt|log))", combined)
|
|
174
|
+
artifacts: list[str] = []
|
|
175
|
+
seen: set[str] = set()
|
|
176
|
+
for candidate in candidates:
|
|
177
|
+
path = Path(candidate)
|
|
178
|
+
resolved = path if path.is_absolute() else (project_root / path)
|
|
179
|
+
try:
|
|
180
|
+
normalized = str(resolved.resolve().relative_to(project_root.resolve())).replace("\\", "/")
|
|
181
|
+
except ValueError:
|
|
182
|
+
normalized = str(resolved.resolve()).replace("\\", "/")
|
|
183
|
+
if not resolved.exists() or normalized in seen:
|
|
184
|
+
continue
|
|
185
|
+
seen.add(normalized)
|
|
186
|
+
artifacts.append(normalized)
|
|
187
|
+
return artifacts
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from telchines.models import Observation
|
|
7
|
+
from telchines.utils import stable_id
|
|
8
|
+
|
|
9
|
+
GENERIC_PATTERNS = [
|
|
10
|
+
re.compile(r"ERROR:\s+(?P<file>.+?):(?P<line>\d+):\s+(?P<message>.+)"),
|
|
11
|
+
re.compile(r"(?P<file>[^:\n]+):(?P<line>\d+):\s*(?P<severity>error|warning):\s*(?P<message>.+)", re.IGNORECASE),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_signature(message: str) -> str:
|
|
16
|
+
text = message.lower()
|
|
17
|
+
if "semicolon" in text:
|
|
18
|
+
return "SV_PARSE_EXPECTED_SEMICOLON"
|
|
19
|
+
if "endmodule" in text:
|
|
20
|
+
return "SV_EXPECTED_ENDMODULE"
|
|
21
|
+
if ("missing end" in text) or ("expecting 'end'" in text) or ("expected end before" in text):
|
|
22
|
+
return "SV_EXPECTED_END"
|
|
23
|
+
if "syntax error" in text:
|
|
24
|
+
return "SV_GENERIC_SYNTAX_ERROR"
|
|
25
|
+
if "undeclared" in text or "unknown identifier" in text:
|
|
26
|
+
return "SV_UNKNOWN_IDENTIFIER"
|
|
27
|
+
if "cannot open" in text or "no such file" in text:
|
|
28
|
+
return "FILE_NOT_FOUND"
|
|
29
|
+
if "timeout" in text:
|
|
30
|
+
return "SIM_TIMEOUT"
|
|
31
|
+
if "assert" in text and "fail" in text:
|
|
32
|
+
return "ASSERTION_FAILURE"
|
|
33
|
+
compact = re.sub(r"[^a-z0-9]+", "_", text.upper()).strip("_")
|
|
34
|
+
return compact[:64] or "UNKNOWN_TOOL_ERROR"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_common_output(run_id: str, text: str, default_type: str = "tool_error") -> list[Observation]:
|
|
38
|
+
observations: list[Observation] = []
|
|
39
|
+
for line_text in text.splitlines():
|
|
40
|
+
for pattern in GENERIC_PATTERNS:
|
|
41
|
+
match = pattern.search(line_text)
|
|
42
|
+
if not match:
|
|
43
|
+
continue
|
|
44
|
+
message = match.group("message").strip()
|
|
45
|
+
observations.append(
|
|
46
|
+
Observation(
|
|
47
|
+
observation_id=stable_id("obs", run_id, line_text),
|
|
48
|
+
run_id=run_id,
|
|
49
|
+
type=default_type,
|
|
50
|
+
signature=normalize_signature(message),
|
|
51
|
+
file=str(Path(match.group("file"))),
|
|
52
|
+
line=int(match.group("line")),
|
|
53
|
+
message=message,
|
|
54
|
+
severity=match.groupdict().get("severity", "error").lower(),
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
break
|
|
58
|
+
return observations
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from telchines.adapters.base import ToolAdapter
|
|
4
|
+
from telchines.adapters.open_tools import IcarusAdapter, SlangAdapter, SymbiYosysAdapter, VeribleAdapter, VerilatorAdapter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AdapterRegistry:
|
|
8
|
+
def __init__(self) -> None:
|
|
9
|
+
self._adapters: dict[str, ToolAdapter] = {
|
|
10
|
+
"verilator": VerilatorAdapter(),
|
|
11
|
+
"iverilog": IcarusAdapter(),
|
|
12
|
+
"slang": SlangAdapter(),
|
|
13
|
+
"verible": VeribleAdapter(),
|
|
14
|
+
"symbiyosys": SymbiYosysAdapter(),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def get(self, name: str) -> ToolAdapter:
|
|
18
|
+
try:
|
|
19
|
+
return self._adapters[name]
|
|
20
|
+
except KeyError as exc:
|
|
21
|
+
raise KeyError(f"unknown adapter: {name}") from exc
|
|
22
|
+
|
|
23
|
+
def list(self, *, category: str | None = None) -> list[ToolAdapter]:
|
|
24
|
+
adapters = list(self._adapters.values())
|
|
25
|
+
if category is not None:
|
|
26
|
+
adapters = [adapter for adapter in adapters if adapter.category == category or adapter.kind == category]
|
|
27
|
+
return sorted(adapters, key=lambda adapter: adapter.name)
|