rote-cli 0.1.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.
- rote/__init__.py +3 -0
- rote/adapters/__init__.py +70 -0
- rote/adapters/_common.py +149 -0
- rote/adapters/cloudflare.py +978 -0
- rote/adapters/dbos.py +1235 -0
- rote/adapters/temporal.py +568 -0
- rote/cli.py +520 -0
- rote/graduator/__init__.py +259 -0
- rote/graduator/drivers/__init__.py +226 -0
- rote/graduator/drivers/anthropic_api.py +423 -0
- rote/graduator/drivers/claude.py +312 -0
- rote/graduator/drivers/codex.py +83 -0
- rote/ir.py +495 -0
- rote/serve/__init__.py +14 -0
- rote/serve/backends.py +162 -0
- rote/serve/registry.py +201 -0
- rote/serve/server.py +249 -0
- rote/skills/rote-graduate/SKILL.md +230 -0
- rote/skills/rote-graduate/references/crystallization-heuristics.md +260 -0
- rote/skills/rote-graduate/references/ir-schema.md +420 -0
- rote/skills/rote-graduate/references/llm-judge-extraction.md +341 -0
- rote/skills/rote-graduate/references/node-kinds.md +223 -0
- rote_cli-0.1.0.dist-info/METADATA +546 -0
- rote_cli-0.1.0.dist-info/RECORD +27 -0
- rote_cli-0.1.0.dist-info/WHEEL +4 -0
- rote_cli-0.1.0.dist-info/entry_points.txt +2 -0
- rote_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""The graduator orchestrator.
|
|
2
|
+
|
|
3
|
+
The :class:`Graduator` is the high-level entry point used by
|
|
4
|
+
``rote graduate``. It:
|
|
5
|
+
|
|
6
|
+
1. Picks a driver (auto-detect or explicit ``--agent``).
|
|
7
|
+
2. Locates the rote-graduate skill bundle.
|
|
8
|
+
3. Creates a temp work directory.
|
|
9
|
+
4. Calls ``driver.run()`` to produce ``pipeline.yaml`` (and any
|
|
10
|
+
extracted modules / signature stubs).
|
|
11
|
+
5. Validates the produced ``pipeline.yaml`` via
|
|
12
|
+
:func:`rote.ir.load_pipeline`.
|
|
13
|
+
6. Moves the work directory contents into the user's output directory.
|
|
14
|
+
7. Returns a :class:`GraduationResult`.
|
|
15
|
+
|
|
16
|
+
This module deliberately knows nothing about specific runtimes
|
|
17
|
+
(Temporal, Inngest, etc.) — that's the adapter layer's job. The
|
|
18
|
+
graduator's only output is the validated IR + the stub files;
|
|
19
|
+
``rote.adapters`` consumes those to emit runnable code.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import shutil
|
|
25
|
+
import tempfile
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from rote.graduator.drivers import (
|
|
31
|
+
DriverError,
|
|
32
|
+
GraduatorDriver,
|
|
33
|
+
auto_detect,
|
|
34
|
+
available_drivers,
|
|
35
|
+
get_driver,
|
|
36
|
+
)
|
|
37
|
+
from rote.ir import Pipeline, load_pipeline
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GraduatorError(RuntimeError):
|
|
41
|
+
"""High-level graduator failure.
|
|
42
|
+
|
|
43
|
+
Wraps driver errors and validation errors with a user-facing
|
|
44
|
+
message. The CLI prints the message to stderr and exits non-zero.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class GraduationResult:
|
|
50
|
+
"""The output of a successful graduation."""
|
|
51
|
+
|
|
52
|
+
pipeline: Pipeline
|
|
53
|
+
output_dir: Path
|
|
54
|
+
driver_name: str
|
|
55
|
+
driver_metadata: dict[str, Any] = field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _default_graduator_skill_dir() -> Path:
|
|
59
|
+
"""Locate the rote-graduate skill bundled with the rote source.
|
|
60
|
+
|
|
61
|
+
Two layouts exist, checked in order:
|
|
62
|
+
|
|
63
|
+
1. **Wheel install.** ``pyproject.toml`` force-includes the
|
|
64
|
+
repo-root ``skills/rote-graduate/`` into the wheel at
|
|
65
|
+
``rote/skills/rote-graduate/``, so the bundle sits next to the
|
|
66
|
+
package's ``__file__``.
|
|
67
|
+
2. **Editable install / source checkout.** The skill lives at
|
|
68
|
+
``<repo>/skills/rote-graduate/``; ``rote.__file__`` is
|
|
69
|
+
``<repo>/src/rote/__init__.py``, so the repo root is three
|
|
70
|
+
parents up.
|
|
71
|
+
"""
|
|
72
|
+
import rote
|
|
73
|
+
|
|
74
|
+
rote_pkg_dir = Path(rote.__file__).resolve().parent
|
|
75
|
+
candidates = [
|
|
76
|
+
rote_pkg_dir / "skills" / "rote-graduate",
|
|
77
|
+
rote_pkg_dir.parent.parent / "skills" / "rote-graduate",
|
|
78
|
+
]
|
|
79
|
+
for candidate in candidates:
|
|
80
|
+
if candidate.is_dir() and (candidate / "SKILL.md").is_file():
|
|
81
|
+
return candidate
|
|
82
|
+
searched = ", ".join(str(c) for c in candidates)
|
|
83
|
+
raise GraduatorError(
|
|
84
|
+
f"Could not locate the rote-graduate skill (searched: {searched}). "
|
|
85
|
+
"Pass an explicit graduator_skill_dir to Graduator() pointing "
|
|
86
|
+
"at a rote-graduate skill bundle."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Graduator:
|
|
91
|
+
"""High-level orchestrator for graduating a skill into a pipeline."""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
agent: str | None = None,
|
|
96
|
+
graduator_skill_dir: Path | None = None,
|
|
97
|
+
model: str | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
agent
|
|
103
|
+
Driver name (``"claude"``, ``"codex"``, ``"api"``) or
|
|
104
|
+
``None`` for auto-detect.
|
|
105
|
+
graduator_skill_dir
|
|
106
|
+
Path to the rote-graduate skill bundle. Defaults to the
|
|
107
|
+
bundled one inside the rote source tree.
|
|
108
|
+
model
|
|
109
|
+
Override the LLM model the driver uses for the graduator
|
|
110
|
+
(e.g. ``"claude-opus-4-6"`` to use Opus instead of the
|
|
111
|
+
default Sonnet). ``None`` uses the driver's default.
|
|
112
|
+
"""
|
|
113
|
+
self.agent = agent
|
|
114
|
+
self._explicit_graduator_skill_dir = graduator_skill_dir
|
|
115
|
+
self.model = model
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def graduator_skill_dir(self) -> Path:
|
|
119
|
+
if self._explicit_graduator_skill_dir is not None:
|
|
120
|
+
return self._explicit_graduator_skill_dir
|
|
121
|
+
return _default_graduator_skill_dir()
|
|
122
|
+
|
|
123
|
+
def select_driver(self) -> GraduatorDriver:
|
|
124
|
+
"""Choose a driver based on the agent setting or auto-detect.
|
|
125
|
+
|
|
126
|
+
When a non-default ``model`` was passed to the ``Graduator``
|
|
127
|
+
constructor, it's forwarded to the driver via ``get_driver``
|
|
128
|
+
kwargs. Auto-detect finds an available driver first (using
|
|
129
|
+
a zero-arg probe), then re-constructs it with the kwargs so
|
|
130
|
+
the override is honored.
|
|
131
|
+
"""
|
|
132
|
+
driver_kwargs: dict[str, object] = {}
|
|
133
|
+
if self.model is not None:
|
|
134
|
+
driver_kwargs["model"] = self.model
|
|
135
|
+
|
|
136
|
+
if self.agent:
|
|
137
|
+
try:
|
|
138
|
+
driver = get_driver(self.agent, **driver_kwargs)
|
|
139
|
+
except KeyError as e:
|
|
140
|
+
raise GraduatorError(str(e.args[0])) from None
|
|
141
|
+
available, reason = driver.is_available()
|
|
142
|
+
if not available:
|
|
143
|
+
raise GraduatorError(f"Driver {self.agent!r} is not available: {reason}")
|
|
144
|
+
return driver
|
|
145
|
+
|
|
146
|
+
probe = auto_detect()
|
|
147
|
+
if probe is None:
|
|
148
|
+
lines = [
|
|
149
|
+
"No graduator driver is available. Tried:",
|
|
150
|
+
]
|
|
151
|
+
for name, _avail, reason in available_drivers():
|
|
152
|
+
lines.append(f" - {name}: {reason}")
|
|
153
|
+
lines.append("")
|
|
154
|
+
lines.append(
|
|
155
|
+
"Install one of the supported coding agent CLIs, or set "
|
|
156
|
+
"ANTHROPIC_API_KEY and `pip install rote[api]`."
|
|
157
|
+
)
|
|
158
|
+
raise GraduatorError("\n".join(lines))
|
|
159
|
+
|
|
160
|
+
# Re-construct the detected driver with user-specified kwargs
|
|
161
|
+
# (e.g. model override) instead of returning the probe instance
|
|
162
|
+
# directly.
|
|
163
|
+
return get_driver(probe.name, **driver_kwargs)
|
|
164
|
+
|
|
165
|
+
async def graduate(
|
|
166
|
+
self,
|
|
167
|
+
skill_dir: Path | str,
|
|
168
|
+
output_dir: Path | str,
|
|
169
|
+
) -> GraduationResult:
|
|
170
|
+
"""Run the full graduation flow.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
skill_dir
|
|
175
|
+
Path to the source skill bundle (must contain a SKILL.md).
|
|
176
|
+
output_dir
|
|
177
|
+
Where to write the graduated artifacts. Created if missing;
|
|
178
|
+
existing files of the same name are overwritten.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
GraduationResult
|
|
183
|
+
A validated :class:`Pipeline` plus the output directory and
|
|
184
|
+
driver metadata.
|
|
185
|
+
|
|
186
|
+
Raises
|
|
187
|
+
------
|
|
188
|
+
GraduatorError
|
|
189
|
+
For any user-actionable failure: missing skill bundle,
|
|
190
|
+
unavailable driver, driver failure, or invalid produced
|
|
191
|
+
pipeline.yaml.
|
|
192
|
+
"""
|
|
193
|
+
skill_dir = Path(skill_dir).resolve()
|
|
194
|
+
output_dir = Path(output_dir).resolve()
|
|
195
|
+
|
|
196
|
+
if not skill_dir.is_dir():
|
|
197
|
+
raise GraduatorError(f"Skill directory does not exist: {skill_dir}")
|
|
198
|
+
if not (skill_dir / "SKILL.md").is_file():
|
|
199
|
+
raise GraduatorError(f"Not a skill bundle (no SKILL.md found): {skill_dir}")
|
|
200
|
+
|
|
201
|
+
driver = self.select_driver()
|
|
202
|
+
|
|
203
|
+
with tempfile.TemporaryDirectory(prefix="rote-graduate-") as work_dir_str:
|
|
204
|
+
work_dir = Path(work_dir_str)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
result = await driver.run(
|
|
208
|
+
skill_dir=skill_dir,
|
|
209
|
+
graduator_skill_dir=self.graduator_skill_dir,
|
|
210
|
+
work_dir=work_dir,
|
|
211
|
+
)
|
|
212
|
+
except DriverError as e:
|
|
213
|
+
detail = f"\n\n{e.details}" if getattr(e, "details", None) else ""
|
|
214
|
+
raise GraduatorError(f"Driver {driver.name!r} failed: {e}{detail}") from e
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
pipeline = load_pipeline(result.pipeline_yaml_path)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
raise GraduatorError(
|
|
220
|
+
f"Driver {driver.name!r} produced an invalid "
|
|
221
|
+
f"pipeline.yaml at {result.pipeline_yaml_path}: {e}"
|
|
222
|
+
) from e
|
|
223
|
+
|
|
224
|
+
self._move_work_dir_to_output(result.work_dir, output_dir)
|
|
225
|
+
|
|
226
|
+
# Re-point the result's path to the moved location for the
|
|
227
|
+
# caller's benefit.
|
|
228
|
+
return GraduationResult(
|
|
229
|
+
pipeline=pipeline,
|
|
230
|
+
output_dir=output_dir,
|
|
231
|
+
driver_name=result.driver_name,
|
|
232
|
+
driver_metadata=result.metadata,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def _move_work_dir_to_output(work_dir: Path, output_dir: Path) -> None:
|
|
237
|
+
"""Move every entry in ``work_dir`` into ``output_dir``.
|
|
238
|
+
|
|
239
|
+
Existing entries with the same name in ``output_dir`` are
|
|
240
|
+
replaced. We use ``shutil.move`` per-entry rather than moving
|
|
241
|
+
the whole directory because ``output_dir`` may already exist
|
|
242
|
+
and may contain unrelated files.
|
|
243
|
+
"""
|
|
244
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
for item in work_dir.iterdir():
|
|
246
|
+
target = output_dir / item.name
|
|
247
|
+
if target.exists():
|
|
248
|
+
if target.is_dir():
|
|
249
|
+
shutil.rmtree(target)
|
|
250
|
+
else:
|
|
251
|
+
target.unlink()
|
|
252
|
+
shutil.move(str(item), str(target))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
__all__ = [
|
|
256
|
+
"Graduator",
|
|
257
|
+
"GraduationResult",
|
|
258
|
+
"GraduatorError",
|
|
259
|
+
]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Graduator drivers — pluggable backends for running the graduator agent.
|
|
2
|
+
|
|
3
|
+
Every driver implements the :class:`GraduatorDriver` Protocol and is
|
|
4
|
+
registered in :data:`DRIVERS`. The CLI dispatches to a driver by name
|
|
5
|
+
(``rote graduate --agent claude``) or by auto-detection
|
|
6
|
+
(:func:`auto_detect`).
|
|
7
|
+
|
|
8
|
+
See ``docs/agent-runtime.md`` for the full design record, including the
|
|
9
|
+
rationale for the three-driver lineup (Claude Code, Codex CLI,
|
|
10
|
+
Anthropic SDK) and the explicit non-use of ``claude-agent-sdk``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Protocol
|
|
19
|
+
|
|
20
|
+
# ───────── Shared types ─────────
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DriverError(RuntimeError):
|
|
24
|
+
"""Raised when a driver fails to produce a graduated pipeline.yaml.
|
|
25
|
+
|
|
26
|
+
The message is user-facing and should explain what went wrong and
|
|
27
|
+
what the user can do about it. Driver implementations should attach
|
|
28
|
+
any captured stdout / stderr via the ``details`` attribute.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str, *, details: str | None = None) -> None:
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.details = details
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class DriverResult:
|
|
38
|
+
"""The output of a successful driver run.
|
|
39
|
+
|
|
40
|
+
Attributes
|
|
41
|
+
----------
|
|
42
|
+
pipeline_yaml_path
|
|
43
|
+
Path to the produced ``pipeline.yaml`` inside ``work_dir``.
|
|
44
|
+
work_dir
|
|
45
|
+
Scratch directory the driver wrote into. May contain additional
|
|
46
|
+
artifacts (``extracted/*.py``, ``signatures/*.py``, etc.) that
|
|
47
|
+
the graduator orchestrator will move into the user's output
|
|
48
|
+
directory.
|
|
49
|
+
driver_name
|
|
50
|
+
The name of the driver that produced this result (e.g.
|
|
51
|
+
``"claude"``, ``"codex"``, ``"api"``).
|
|
52
|
+
metadata
|
|
53
|
+
Free-form dict of driver-specific metadata: token counts,
|
|
54
|
+
estimated cost, duration, model name, session id, etc. Used for
|
|
55
|
+
the Phase 7 graduation report but not the IR itself.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
pipeline_yaml_path: Path
|
|
59
|
+
work_dir: Path
|
|
60
|
+
driver_name: str
|
|
61
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class GraduatorDriver(Protocol):
|
|
65
|
+
"""Protocol every graduator driver implements.
|
|
66
|
+
|
|
67
|
+
The driver's only responsibility is: **given a source skill and the
|
|
68
|
+
rote-graduate rubric, run the agent loop and make sure
|
|
69
|
+
``work_dir/pipeline.yaml`` exists when it returns.** How it gets
|
|
70
|
+
there — subprocess, in-process, remote — is the driver's business.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
name: str
|
|
74
|
+
"""Short identifier used in the CLI (``--agent <name>``)."""
|
|
75
|
+
|
|
76
|
+
def is_available(self) -> tuple[bool, str]:
|
|
77
|
+
"""Check whether this driver can run right now.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
tuple[bool, str]
|
|
82
|
+
``(available, reason)``. When ``available`` is ``True``, the
|
|
83
|
+
reason may be an empty string. When ``False``, ``reason`` is
|
|
84
|
+
a user-facing message explaining what needs to happen
|
|
85
|
+
(install the CLI, run login, set an env var).
|
|
86
|
+
"""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
async def run(
|
|
90
|
+
self,
|
|
91
|
+
skill_dir: Path,
|
|
92
|
+
graduator_skill_dir: Path,
|
|
93
|
+
work_dir: Path,
|
|
94
|
+
) -> DriverResult:
|
|
95
|
+
"""Run the graduator agent against ``skill_dir``.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
skill_dir
|
|
100
|
+
The source skill bundle to graduate (read-only to the agent).
|
|
101
|
+
graduator_skill_dir
|
|
102
|
+
The ``rote-graduate`` skill with SKILL.md and references/.
|
|
103
|
+
work_dir
|
|
104
|
+
A scratch directory where the agent writes ``pipeline.yaml``
|
|
105
|
+
and any extracted/signature stubs. The caller ensures this
|
|
106
|
+
directory exists and is empty.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
DriverResult
|
|
111
|
+
A result pointing at ``work_dir/pipeline.yaml``.
|
|
112
|
+
|
|
113
|
+
Raises
|
|
114
|
+
------
|
|
115
|
+
DriverError
|
|
116
|
+
If the driver cannot produce a valid ``pipeline.yaml``.
|
|
117
|
+
"""
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ───────── Driver registry ─────────
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _claude_driver_factory(**kwargs: Any) -> GraduatorDriver:
|
|
125
|
+
from rote.graduator.drivers.claude import ClaudeDriver
|
|
126
|
+
|
|
127
|
+
return ClaudeDriver(**kwargs)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _codex_driver_factory(**kwargs: Any) -> GraduatorDriver:
|
|
131
|
+
from rote.graduator.drivers.codex import CodexDriver
|
|
132
|
+
|
|
133
|
+
return CodexDriver(**kwargs)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _anthropic_driver_factory(**kwargs: Any) -> GraduatorDriver:
|
|
137
|
+
from rote.graduator.drivers.anthropic_api import AnthropicApiDriver
|
|
138
|
+
|
|
139
|
+
return AnthropicApiDriver(**kwargs)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
#: Name → factory. Keep the values lazy so we don't pay the import cost
|
|
143
|
+
#: of the anthropic SDK on every CLI invocation. Factories accept
|
|
144
|
+
#: ``**kwargs`` which are forwarded to the driver constructor (e.g.
|
|
145
|
+
#: ``model``, ``max_turns``).
|
|
146
|
+
DRIVERS: dict[str, Callable[..., GraduatorDriver]] = {
|
|
147
|
+
"claude": _claude_driver_factory,
|
|
148
|
+
"codex": _codex_driver_factory,
|
|
149
|
+
"api": _anthropic_driver_factory,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#: Order used by :func:`auto_detect`. Claude first because Claude Max/Pro
|
|
153
|
+
#: is the most likely subscription rote's audience already has.
|
|
154
|
+
AUTO_DETECT_ORDER: tuple[str, ...] = ("claude", "codex", "api")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_driver(name: str, **kwargs: Any) -> GraduatorDriver:
|
|
158
|
+
"""Return a driver instance by name, configured with ``kwargs``.
|
|
159
|
+
|
|
160
|
+
``kwargs`` are forwarded to the driver's constructor (e.g. ``model``
|
|
161
|
+
for ``ClaudeDriver`` and ``AnthropicApiDriver``). Drivers ignore
|
|
162
|
+
kwargs they don't understand via ``**kwargs`` absorption in their
|
|
163
|
+
factories.
|
|
164
|
+
|
|
165
|
+
Raises :class:`KeyError` with a helpful message when the name is
|
|
166
|
+
unknown.
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
factory = DRIVERS[name]
|
|
170
|
+
except KeyError:
|
|
171
|
+
available = ", ".join(sorted(DRIVERS))
|
|
172
|
+
raise KeyError(f"Unknown agent driver {name!r}. Available: {available}") from None
|
|
173
|
+
return factory(**kwargs)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def auto_detect() -> GraduatorDriver | None:
|
|
177
|
+
"""Return the first available driver in :data:`AUTO_DETECT_ORDER`.
|
|
178
|
+
|
|
179
|
+
Returns ``None`` when no driver is available. The CLI translates
|
|
180
|
+
that into a helpful error listing setup instructions for each
|
|
181
|
+
driver.
|
|
182
|
+
"""
|
|
183
|
+
for name in AUTO_DETECT_ORDER:
|
|
184
|
+
try:
|
|
185
|
+
driver = DRIVERS[name]()
|
|
186
|
+
except Exception:
|
|
187
|
+
# A factory raising at import time (e.g. a missing optional
|
|
188
|
+
# dep that isn't gracefully handled) should never block
|
|
189
|
+
# auto-detect from trying the next driver.
|
|
190
|
+
continue
|
|
191
|
+
try:
|
|
192
|
+
available, _reason = driver.is_available()
|
|
193
|
+
except Exception:
|
|
194
|
+
continue
|
|
195
|
+
if available:
|
|
196
|
+
return driver
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def available_drivers() -> list[tuple[str, bool, str]]:
|
|
201
|
+
"""Return a diagnostic list of ``(name, available, reason)`` triples.
|
|
202
|
+
|
|
203
|
+
Used by the CLI's ``--agent auto`` failure path to produce a clear
|
|
204
|
+
message telling the user what each driver needs.
|
|
205
|
+
"""
|
|
206
|
+
out: list[tuple[str, bool, str]] = []
|
|
207
|
+
for name in AUTO_DETECT_ORDER:
|
|
208
|
+
try:
|
|
209
|
+
driver = DRIVERS[name]()
|
|
210
|
+
available, reason = driver.is_available()
|
|
211
|
+
out.append((name, available, reason))
|
|
212
|
+
except Exception as e:
|
|
213
|
+
out.append((name, False, f"driver failed to load: {e}"))
|
|
214
|
+
return out
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
__all__ = [
|
|
218
|
+
"DriverError",
|
|
219
|
+
"DriverResult",
|
|
220
|
+
"GraduatorDriver",
|
|
221
|
+
"DRIVERS",
|
|
222
|
+
"AUTO_DETECT_ORDER",
|
|
223
|
+
"get_driver",
|
|
224
|
+
"auto_detect",
|
|
225
|
+
"available_drivers",
|
|
226
|
+
]
|