Skill Tracer
Skills › Scientific Agent Skills › pylabrobot › inspect_backends.py

inspect_backends.py

223 lines · skills/pylabrobot/scripts/inspect_backends.py at 5f2bc03

1#!/usr/bin/env python3
2"""Inspect a pinned PyLabRobot API surface without constructing a backend."""
3
4from __future__ import annotations
5
6import argparse
7import inspect
8import re
9from importlib.metadata import PackageNotFoundError, metadata, version
10from typing import Any, Sequence
12if __package__:
13 from ._common import PYLABROBOT_VERSION, ValidationError, emit_error, emit_json
14else:
15 from _common import PYLABROBOT_VERSION, ValidationError, emit_error, emit_json
17VERSION_RE = re.compile(r"^[0-9]+(?:\.[0-9]+){2}$")
20def build_parser() -> argparse.ArgumentParser:
21 parser = argparse.ArgumentParser(
22 description=(
23 "Inspect installed PyLabRobot version, known backend symbols, frontend "
24 "methods, and release availability. Imports are lazy; no backend is "
25 "instantiated and no setup, serial, USB, or network operation is performed."
26 )
27 )
28 parser.add_argument(
29 "--expected-version",
30 default=PYLABROBOT_VERSION,
31 help=f"Expected exact stable version (default: {PYLABROBOT_VERSION})",
32 )
33 parser.add_argument(
34 "--strict",
35 action="store_true",
36 help="Exit nonzero when PyLabRobot is absent, mismatched, or symbols fail to import",
37 )
38 return parser
41def _class_record(
42 class_object: type[Any],
43 *,
44 support: str,
45 transport: str,
46 methods: Sequence[str],
47) -> dict[str, Any]:
48 return {
49 "class": class_object.__name__,
50 "import_path": f"{class_object.__module__}.{class_object.__name__}",
51 "signature": str(inspect.signature(class_object)),
52 "support_note": support,
53 "transport": transport,
54 "declared_methods": {
55 method: hasattr(class_object, method)
56 for method in methods
57 },
58 }
61def inspect_installation(expected_version: str) -> dict[str, Any]:
62 if VERSION_RE.fullmatch(expected_version) is None:
63 raise ValidationError("--expected-version must use X.Y.Z numeric form")
64 try:
65 installed_version = version("PyLabRobot")
66 package_metadata = metadata("PyLabRobot")
67 except PackageNotFoundError:
68 return {
69 "ok": False,
70 "installed": False,
71 "expected_version": expected_version,
72 "connection_attempted": False,
73 "serial_usb_network_access": False,
74 "message": "PyLabRobot is not installed; help and local planning CLIs remain available.",
75 }
77 import_errors: list[str] = []
78 backends: list[dict[str, Any]] = []
79 frontends: dict[str, dict[str, bool]] = {}
80 try:
81 from pylabrobot.liquid_handling import LiquidHandler
82 from pylabrobot.liquid_handling.backends import (
83 EVOBackend,
84 LiquidHandlerChatterboxBackend,
85 OpentronsOT2Backend,
86 STARBackend,
87 VantageBackend,
88 )
90 operation_methods = (
91 "setup",
92 "stop",
93 "pick_up_tips",
94 "drop_tips",
95 "aspirate",
96 "dispense",
97 )
98 backends = [
99 _class_record(
100 LiquidHandlerChatterboxBackend,
101 support="testing/offline",
102 transport="console only",
103 methods=operation_methods,
104 ),
105 _class_record(
106 STARBackend,
107 support="released symbol; model capability requires review",
108 transport="vendor firmware over USB",
109 methods=operation_methods,
110 ),
111 _class_record(
112 VantageBackend,
113 support="released symbol; model capability requires review",
114 transport="vendor firmware over USB",
115 methods=operation_methods,
116 ),
117 _class_record(
118 EVOBackend,
119 support="released symbol; model capability requires review",
120 transport="vendor firmware over USB",
121 methods=operation_methods,
122 ),
123 _class_record(
124 OpentronsOT2Backend,
125 support="released symbol; model capability requires review",
126 transport="HTTP to explicitly configured host",
127 methods=operation_methods,
128 ),
129 ]
130 frontends["LiquidHandler"] = {
131 method: hasattr(LiquidHandler, method)
132 for method in (
133 "setup",
134 "stop",
135 "pick_up_tips",
136 "drop_tips",
137 "return_tips",
138 "aspirate",
139 "dispense",
140 "transfer",
141 )
142 }
143 except (ImportError, AttributeError) as error:
144 import_errors.append(f"liquid_handling: {type(error).__name__}: {error}")
146 try:
147 from pylabrobot.centrifuge import Centrifuge
148 from pylabrobot.heating_shaking import HeaterShaker
149 from pylabrobot.plate_reading import PlateReader
150 from pylabrobot.pumps import Pump
151 from pylabrobot.scales import Scale
152 from pylabrobot.shaking import Shaker
153 from pylabrobot.temperature_controlling import TemperatureController
154 from pylabrobot.visualizer import Visualizer
156 method_map: dict[str, tuple[type[Any], tuple[str, ...]]] = {
157 "PlateReader": (
158 PlateReader,
159 ("setup", "stop", "open", "close", "read_absorbance", "read_fluorescence"),
160 ),
161 "Pump": (Pump, ("setup", "stop", "run_for_duration", "run_continuously", "pump_volume", "halt")),
162 "Scale": (Scale, ("setup", "stop", "read_weight", "tare", "zero")),
163 "HeaterShaker": (
164 HeaterShaker,
165 ("setup", "stop", "set_temperature", "shake", "stop_shaking"),
166 ),
167 "Shaker": (Shaker, ("setup", "stop", "shake", "stop_shaking")),
168 "TemperatureController": (
169 TemperatureController,
170 ("setup", "stop", "set_temperature", "get_temperature", "deactivate"),
171 ),
172 "Centrifuge": (
173 Centrifuge,
174 ("setup", "stop", "open_door", "close_door", "lock_door", "spin"),
175 ),
176 "Visualizer": (Visualizer, ("setup", "stop")),
177 }
178 for name, (class_object, methods) in method_map.items():
179 frontends[name] = {method: hasattr(class_object, method) for method in methods}
180 except (ImportError, AttributeError) as error:
181 import_errors.append(f"equipment: {type(error).__name__}: {error}")
183 missing_methods = [
184 f"{name}.{method}"
185 for name, methods in frontends.items()
186 for method, present in methods.items()
187 if not present
188 ]
189 return {
190 "ok": not import_errors and not missing_methods and installed_version == expected_version,
191 "installed": True,
192 "installed_version": installed_version,
193 "expected_version": expected_version,
194 "version_match": installed_version == expected_version,
195 "requires_python": package_metadata.get("Requires-Python"),
196 "connection_attempted": False,
197 "serial_usb_network_access": False,
198 "backend_instances_created": 0,
199 "backends": backends,
200 "frontends": frontends,
201 "import_errors": import_errors,
202 "missing_methods": missing_methods,
203 "stable_status_note": (
204 "Exports inspected against PyLabRobot 0.2.2; hosted support tables can describe development APIs. "
205 "availability and capabilities remain model/firmware/configuration specific."
206 ),
207 }
210def main(argv: Sequence[str] | None = None) -> int:
211 args = build_parser().parse_args(argv)
212 try:
213 report = inspect_installation(args.expected_version)
214 except ValidationError as error:
215 return emit_error(error)
216 emit_json(report)
217 if args.strict and not report.get("ok", False):
218 return 4
219 return 0
222if __name__ == "__main__":
223 raise SystemExit(main())