rhylthyme-cli-runner 0.2.8a0__tar.gz → 0.3.0a0__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.
- {rhylthyme_cli_runner-0.2.8a0/src/rhylthyme_cli_runner.egg-info → rhylthyme_cli_runner-0.3.0a0}/PKG-INFO +2 -2
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/setup.py +3 -3
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/__init__.py +1 -1
- rhylthyme_cli_runner-0.3.0a0/src/rhylthyme_cli_runner/bridge_serve.py +273 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/cli.py +95 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/recorder.py +17 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/instruments.py +84 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/program_runner.py +185 -22
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0/src/rhylthyme_cli_runner.egg-info}/PKG-INFO +2 -2
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner.egg-info/SOURCES.txt +2 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner.egg-info/requires.txt +1 -1
- rhylthyme_cli_runner-0.3.0a0/tests/test_bridge_serve.py +293 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_instruments.py +258 -2
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/LICENSE +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/README.md +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/pyproject.toml +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/setup.cfg +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/environment_icons.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/environment_loader.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/environment_schemas.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/__init__.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/cli.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/compare.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/gold.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/harness.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/llm.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/matcher.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/metrics.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/patterns/__init__.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/patterns/authoring_guide.md +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.md +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/patterns/extract.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/patterns/four_turn.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/eval/report.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/expand_replicates.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/__init__.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/calibrate.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/calibrate_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/cli.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/evaluate.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/evaluate_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/factors.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/hash.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/predict.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/render.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/replay.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/report.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/report_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/store.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/synth.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/history/usable.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/import_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/instance_checks.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/program_planner.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/remote/__init__.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/remote/auth.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/remote/checks.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/remote/cli.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/remote/mcp_client.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/render_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/validate_program.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner.egg-info/dependency_links.txt +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner.egg-info/entry_points.txt +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner.egg-info/top_level.txt +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_calibrate.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_eval_baseline.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_eval_harness.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_eval_providers.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_eval_scorer.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_evaluate.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_example_validation.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_examples_integration.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_expand_replicates.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_hash_parity.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_history_report.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_import_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_mcp_checks.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_planner_time_strings.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_predict.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_program_runner.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_remote.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_render_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_replay.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_run_recorder.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_skill.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_synth.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_usable.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_validate_examples_ci.py +0 -0
- {rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/tests/test_validate_program.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rhylthyme-cli-runner
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0a0
|
|
4
4
|
Summary: CLI runner for Rhylthyme real-time program schedules
|
|
5
5
|
Home-page: https://github.com/rhylthyme/rhylthyme-cli-runner
|
|
6
6
|
Author: Rhylthyme Team
|
|
@@ -28,7 +28,7 @@ Requires-Dist: pyyaml>=6.0
|
|
|
28
28
|
Requires-Dist: colorama>=0.4.0
|
|
29
29
|
Requires-Dist: rhylthyme-spec>=0.2.1a0
|
|
30
30
|
Provides-Extra: galago
|
|
31
|
-
Requires-Dist: rhylthyme-galago>=0.
|
|
31
|
+
Requires-Dist: rhylthyme-galago>=0.2.0a0; extra == "galago"
|
|
32
32
|
Provides-Extra: dev
|
|
33
33
|
Requires-Dist: pytest>=6.0.0; extra == "dev"
|
|
34
34
|
Requires-Dist: pytest-cov>=2.10.0; extra == "dev"
|
|
@@ -19,7 +19,7 @@ def read_readme():
|
|
|
19
19
|
|
|
20
20
|
setup(
|
|
21
21
|
name="rhylthyme-cli-runner",
|
|
22
|
-
version="0.
|
|
22
|
+
version="0.3.0a0",
|
|
23
23
|
description="CLI runner for Rhylthyme real-time program schedules",
|
|
24
24
|
long_description=read_readme(),
|
|
25
25
|
long_description_content_type="text/markdown",
|
|
@@ -41,8 +41,8 @@ setup(
|
|
|
41
41
|
"rhylthyme-spec>=0.2.1a0",
|
|
42
42
|
],
|
|
43
43
|
extras_require={
|
|
44
|
-
# Instrument steps on galago-tools lab instruments (--workcell)
|
|
45
|
-
"galago": ["rhylthyme-galago>=0.
|
|
44
|
+
# Instrument steps on galago-tools lab instruments (--workcell, rhylthyme bridge)
|
|
45
|
+
"galago": ["rhylthyme-galago>=0.2.0a0"],
|
|
46
46
|
"dev": [
|
|
47
47
|
# Testing framework
|
|
48
48
|
"pytest>=6.0.0",
|
{rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/__init__.py
RENAMED
|
@@ -6,7 +6,7 @@ This package provides the command-line interface for running and validating
|
|
|
6
6
|
Rhylthyme real-time program schedules.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = "0.
|
|
9
|
+
__version__ = "0.3.0a0"
|
|
10
10
|
__author__ = "Rhylthyme Team"
|
|
11
11
|
__description__ = "CLI runner for Rhylthyme real-time program schedules"
|
|
12
12
|
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``rhylthyme bridge --workcell lab.json`` with no program: wait for runs started
|
|
3
|
+
from rhylthyme.com (the web bridge, slice 3; design: rhylthyme-galago
|
|
4
|
+
docs/design/web-bridge.md).
|
|
5
|
+
|
|
6
|
+
The bridge stays listed as online on the Bridges page. A ``start_run`` command
|
|
7
|
+
names a program saved in the user's own library; this machine loads it,
|
|
8
|
+
checks it against the local workcell and refuses it unless it is safe to run
|
|
9
|
+
here, then runs it exactly as ``rhylthyme bridge PROGRAM`` would, and goes
|
|
10
|
+
back to waiting when the run completes or is aborted.
|
|
11
|
+
|
|
12
|
+
Live runs need two keys: the bridge must have been started with
|
|
13
|
+
``--allow-live`` (a decision made at the lab machine), and the browser must
|
|
14
|
+
send the ``live`` the user typed after seeing the pre-flight summary. Before
|
|
15
|
+
a run is accepted, in either mode, its tools are configured and must report
|
|
16
|
+
ready; if not, the start is refused with the pre-flight report (tool addresses
|
|
17
|
+
scrubbed).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import queue
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
from .instruments import INSTALL_HINT, InstrumentSetupError
|
|
27
|
+
|
|
28
|
+
UUID_RE = re.compile(
|
|
29
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.I
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
Outcome = Dict[str, Any]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _no(reason: str) -> Outcome:
|
|
36
|
+
return {"accepted": False, "reason": reason}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _has_code_blocks(program: Mapping[str, Any]) -> bool:
|
|
40
|
+
for group in ("tracks", "trackTemplates"):
|
|
41
|
+
for track in program.get(group) or []:
|
|
42
|
+
for step in track.get("steps") or []:
|
|
43
|
+
if isinstance(step, Mapping) and step.get("codeBlock"):
|
|
44
|
+
return True
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check_start(
|
|
49
|
+
args: Mapping[str, Any],
|
|
50
|
+
fetch_program: Callable[[str], Optional[Mapping[str, Any]]],
|
|
51
|
+
workcell_source: Any,
|
|
52
|
+
schema: Mapping[str, Any],
|
|
53
|
+
allow_live: bool = False,
|
|
54
|
+
) -> Tuple[Outcome, Optional[Dict[str, Any]]]:
|
|
55
|
+
"""
|
|
56
|
+
Decide whether a ``start_run`` may run here. Returns (outcome, program):
|
|
57
|
+
the program when accepted, None otherwise.
|
|
58
|
+
"""
|
|
59
|
+
from .validate_program import perform_additional_validations, validate_program
|
|
60
|
+
|
|
61
|
+
mode = args.get("mode") or "simulated"
|
|
62
|
+
if mode not in ("simulated", "live"):
|
|
63
|
+
return _no(f"unknown mode {mode!r}"), None
|
|
64
|
+
if mode == "live":
|
|
65
|
+
if not allow_live:
|
|
66
|
+
return (
|
|
67
|
+
_no(
|
|
68
|
+
"this bridge does not accept live runs (start it with --allow-live)"
|
|
69
|
+
),
|
|
70
|
+
None,
|
|
71
|
+
)
|
|
72
|
+
if args.get("confirm") != "live":
|
|
73
|
+
return _no("a live run needs the typed confirmation"), None
|
|
74
|
+
program_id = str(args.get("program_id") or "")
|
|
75
|
+
if not UUID_RE.match(program_id):
|
|
76
|
+
return _no("program_id must be the id of a program in your library"), None
|
|
77
|
+
row = fetch_program(program_id)
|
|
78
|
+
if not row or not isinstance(row.get("program_json"), dict):
|
|
79
|
+
return _no("no such program in your library"), None
|
|
80
|
+
program = json.loads(json.dumps(row["program_json"]))
|
|
81
|
+
if _has_code_blocks(program):
|
|
82
|
+
return _no("programs with code blocks cannot be started from the web"), None
|
|
83
|
+
is_valid, schema_errors = validate_program(program, dict(schema))
|
|
84
|
+
errors = list(schema_errors) + perform_additional_validations(
|
|
85
|
+
program, workcell=workcell_source
|
|
86
|
+
)
|
|
87
|
+
if not is_valid or errors:
|
|
88
|
+
shown = "; ".join(str(e) for e in errors[:3])
|
|
89
|
+
more = f" (+{len(errors) - 3} more)" if len(errors) > 3 else ""
|
|
90
|
+
return _no(f"the program is not valid on this workcell: {shown}{more}"), None
|
|
91
|
+
return {"accepted": True, "reason": ""}, program
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def serve(
|
|
95
|
+
workcell_source: Any,
|
|
96
|
+
*,
|
|
97
|
+
schema_file: str,
|
|
98
|
+
time_scale: float = 1.0,
|
|
99
|
+
record: bool = True,
|
|
100
|
+
rest=None,
|
|
101
|
+
token_fn: Optional[Callable[[], str]] = None,
|
|
102
|
+
config_path=None,
|
|
103
|
+
run: Optional[Callable[..., Any]] = None,
|
|
104
|
+
max_runs: Optional[int] = None,
|
|
105
|
+
out=None,
|
|
106
|
+
allow_live: bool = False,
|
|
107
|
+
client_factory: Optional[Callable] = None,
|
|
108
|
+
) -> int:
|
|
109
|
+
"""Wait for runs from the web until Ctrl-C (or ``max_runs``). Returns runs started."""
|
|
110
|
+
out = out or sys.stdout
|
|
111
|
+
try:
|
|
112
|
+
import rhylthyme_galago as galago
|
|
113
|
+
from rhylthyme_galago import bridge as B
|
|
114
|
+
except ImportError:
|
|
115
|
+
raise InstrumentSetupError(INSTALL_HINT) from None
|
|
116
|
+
|
|
117
|
+
from .remote import auth
|
|
118
|
+
from .validate_program import load_program_file
|
|
119
|
+
|
|
120
|
+
if run is None:
|
|
121
|
+
from .program_runner import run_program
|
|
122
|
+
|
|
123
|
+
run = run_program
|
|
124
|
+
run_fn: Callable[..., Any] = run
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
workcell = galago.load_workcell(workcell_source)
|
|
128
|
+
except galago.WorkcellError as e:
|
|
129
|
+
raise InstrumentSetupError(str(e)) from None
|
|
130
|
+
try:
|
|
131
|
+
if rest is None or token_fn is None:
|
|
132
|
+
creds = auth.load_credentials() or {}
|
|
133
|
+
if not creds.get("supabase_url"):
|
|
134
|
+
raise InstrumentSetupError(
|
|
135
|
+
"rhylthyme bridge needs you signed in: run `rhylthyme login`."
|
|
136
|
+
)
|
|
137
|
+
token_fn = token_fn or auth.access_token
|
|
138
|
+
rest = rest or B.SupabaseRest(
|
|
139
|
+
creds["supabase_url"], creds["supabase_anon_key"], token_fn
|
|
140
|
+
)
|
|
141
|
+
user_id = B.user_id_from_token(token_fn())
|
|
142
|
+
except (auth.AuthError, B.BridgeError) as e:
|
|
143
|
+
raise InstrumentSetupError(str(e)) from None
|
|
144
|
+
|
|
145
|
+
bridge_id = B.bridge_id_for(
|
|
146
|
+
workcell, config_path or (auth.config_dir() / "bridges.json")
|
|
147
|
+
)
|
|
148
|
+
schema = load_program_file(schema_file)
|
|
149
|
+
tools: List[Dict[str, str]] = [
|
|
150
|
+
{"name": t.name, "type": t.type, "status": "idle"}
|
|
151
|
+
for t in workcell.tools.values()
|
|
152
|
+
]
|
|
153
|
+
starts: "queue.Queue[Tuple[Dict[str, Any], str, Any, bool]]" = queue.Queue()
|
|
154
|
+
scrub = B.scrubber(workcell)
|
|
155
|
+
|
|
156
|
+
def fetch_program(program_id: str) -> Optional[Mapping[str, Any]]:
|
|
157
|
+
rows = rest.select(
|
|
158
|
+
"programs",
|
|
159
|
+
f"id=eq.{program_id}&user_id=eq.{user_id}&select=id,name,program_json",
|
|
160
|
+
)
|
|
161
|
+
return rows[0] if rows else None
|
|
162
|
+
|
|
163
|
+
def submit(command: Dict[str, Any]) -> Outcome:
|
|
164
|
+
if not starts.empty():
|
|
165
|
+
return _no("a run is already starting")
|
|
166
|
+
args = command.get("args") or {}
|
|
167
|
+
try:
|
|
168
|
+
outcome, program = check_start(
|
|
169
|
+
args, fetch_program, workcell_source, schema, allow_live=allow_live
|
|
170
|
+
)
|
|
171
|
+
except B.BridgeError as e:
|
|
172
|
+
return _no(f"could not load the program: {e}")
|
|
173
|
+
if not outcome["accepted"] or program is None:
|
|
174
|
+
print(f"Refused a run from the web: {outcome['reason']}", file=out)
|
|
175
|
+
return outcome
|
|
176
|
+
live = args.get("mode") == "live"
|
|
177
|
+
# Configure the tools now, so a tool that is not ready is a refusal
|
|
178
|
+
# the browser sees rather than a run that never starts.
|
|
179
|
+
from .instruments import open_instruments
|
|
180
|
+
|
|
181
|
+
session = None
|
|
182
|
+
try:
|
|
183
|
+
kwargs: Dict[str, Any] = {"live": live}
|
|
184
|
+
if client_factory is not None:
|
|
185
|
+
kwargs["client_factory"] = client_factory
|
|
186
|
+
session = open_instruments(program, workcell_source, **kwargs)
|
|
187
|
+
session.prepare()
|
|
188
|
+
except InstrumentSetupError as e:
|
|
189
|
+
if session is not None:
|
|
190
|
+
session.shutdown()
|
|
191
|
+
reason = "tools not ready: " + scrub(str(e)).replace("\n", "; ")
|
|
192
|
+
print(f"Refused a run from the web: {reason}", file=out)
|
|
193
|
+
return _no(reason)
|
|
194
|
+
starts.put((program, args["program_id"], session, live))
|
|
195
|
+
if live:
|
|
196
|
+
print(
|
|
197
|
+
"\n*** LIVE run started from the web: real hardware will move. "
|
|
198
|
+
"Ctrl-C stops it. ***",
|
|
199
|
+
file=out,
|
|
200
|
+
)
|
|
201
|
+
print(
|
|
202
|
+
f"Starting {program.get('name')!r} from the web "
|
|
203
|
+
f"({'LIVE' if live else 'simulated'}).",
|
|
204
|
+
file=out,
|
|
205
|
+
)
|
|
206
|
+
return {"accepted": True, "reason": ""}
|
|
207
|
+
|
|
208
|
+
runs = 0
|
|
209
|
+
while max_runs is None or runs < max_runs:
|
|
210
|
+
idle = B.Publisher(
|
|
211
|
+
None,
|
|
212
|
+
rest=rest,
|
|
213
|
+
bridge_id=bridge_id,
|
|
214
|
+
user_id=user_id,
|
|
215
|
+
workcell=workcell,
|
|
216
|
+
tools=tools,
|
|
217
|
+
allows_live=allow_live,
|
|
218
|
+
version=getattr(galago, "__version__", ""),
|
|
219
|
+
on_error=lambda message: print(f"Bridge: {message}", file=out),
|
|
220
|
+
submit=submit,
|
|
221
|
+
)
|
|
222
|
+
try:
|
|
223
|
+
idle.start()
|
|
224
|
+
except B.BridgeError as e:
|
|
225
|
+
raise InstrumentSetupError(
|
|
226
|
+
f"Could not publish to rhylthyme.com: {e}"
|
|
227
|
+
) from None
|
|
228
|
+
print(
|
|
229
|
+
f"Bridge {workcell.name!r} is waiting for runs from rhylthyme.com "
|
|
230
|
+
"(Bridges page). Ctrl-C stops it.",
|
|
231
|
+
file=out,
|
|
232
|
+
)
|
|
233
|
+
if allow_live:
|
|
234
|
+
print(
|
|
235
|
+
"LIVE runs may be started from the web (--allow-live); each needs "
|
|
236
|
+
"the user to type 'live' after the pre-flight summary.",
|
|
237
|
+
file=out,
|
|
238
|
+
)
|
|
239
|
+
try:
|
|
240
|
+
while True:
|
|
241
|
+
try:
|
|
242
|
+
program, program_id, session, live = starts.get(timeout=0.5)
|
|
243
|
+
break
|
|
244
|
+
except queue.Empty:
|
|
245
|
+
continue
|
|
246
|
+
finally:
|
|
247
|
+
idle.stop()
|
|
248
|
+
try:
|
|
249
|
+
run_fn(
|
|
250
|
+
None,
|
|
251
|
+
schema_file,
|
|
252
|
+
time_scale,
|
|
253
|
+
False, # validated in check_start
|
|
254
|
+
# auto_start=False: the clock runs as soon as the runner opens.
|
|
255
|
+
# (auto_start=True honours a program's manual start trigger and
|
|
256
|
+
# would wait for someone to press 's' at the lab machine.)
|
|
257
|
+
False,
|
|
258
|
+
None,
|
|
259
|
+
record=record,
|
|
260
|
+
factor_prompt=False,
|
|
261
|
+
workcell=workcell_source,
|
|
262
|
+
bridge=True,
|
|
263
|
+
program_data=program,
|
|
264
|
+
program_id=program_id,
|
|
265
|
+
exit_when_done=True,
|
|
266
|
+
live=live,
|
|
267
|
+
prepared_instruments=session,
|
|
268
|
+
bridge_allows_live=allow_live,
|
|
269
|
+
)
|
|
270
|
+
except SystemExit:
|
|
271
|
+
print("The run could not start; waiting for the next one.", file=out)
|
|
272
|
+
runs += 1
|
|
273
|
+
return runs
|
{rhylthyme_cli_runner-0.2.8a0 → rhylthyme_cli_runner-0.3.0a0}/src/rhylthyme_cli_runner/cli.py
RENAMED
|
@@ -360,6 +360,101 @@ def run(
|
|
|
360
360
|
)
|
|
361
361
|
|
|
362
362
|
|
|
363
|
+
@cli.command()
|
|
364
|
+
@click.argument("program_file", type=click.Path(exists=True), required=False)
|
|
365
|
+
@click.option(
|
|
366
|
+
"--workcell",
|
|
367
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
368
|
+
required=True,
|
|
369
|
+
help="Workcell file mapping the program's instrument tools to galago-tools servers",
|
|
370
|
+
)
|
|
371
|
+
@click.option(
|
|
372
|
+
"--live",
|
|
373
|
+
is_flag=True,
|
|
374
|
+
help="Run on real hardware (asks first, as with rhylthyme run --live)",
|
|
375
|
+
)
|
|
376
|
+
@click.option(
|
|
377
|
+
"--confirm-live", is_flag=True, help="Answer the --live question in advance"
|
|
378
|
+
)
|
|
379
|
+
@click.option(
|
|
380
|
+
"--allow-live",
|
|
381
|
+
is_flag=True,
|
|
382
|
+
help=(
|
|
383
|
+
"Waiting for the web: also accept LIVE runs (each still needs the user "
|
|
384
|
+
"to type 'live' in the browser after the pre-flight summary)"
|
|
385
|
+
),
|
|
386
|
+
)
|
|
387
|
+
@click.option(
|
|
388
|
+
"--time-scale", type=float, default=1.0, help="Time scale factor (default: 1.0)"
|
|
389
|
+
)
|
|
390
|
+
@click.option(
|
|
391
|
+
"--no-record",
|
|
392
|
+
is_flag=True,
|
|
393
|
+
help="Do not write a run record when the run ends",
|
|
394
|
+
)
|
|
395
|
+
def bridge(
|
|
396
|
+
program_file, workcell, live, confirm_live, allow_live, time_scale, no_record
|
|
397
|
+
):
|
|
398
|
+
"""
|
|
399
|
+
Run a program on a galago workcell and show it live on rhylthyme.com.
|
|
400
|
+
|
|
401
|
+
Without PROGRAM_FILE, wait for runs started from the Bridges page: a
|
|
402
|
+
program saved in your library, checked here against the workcell (no code
|
|
403
|
+
blocks; its tools must come up ready), run, then back to waiting. Runs are
|
|
404
|
+
simulated unless you pass --allow-live here AND the user types 'live' in
|
|
405
|
+
the browser. Ctrl-C stops.
|
|
406
|
+
|
|
407
|
+
Works like `rhylthyme run --workcell`, and while the run lasts keeps your
|
|
408
|
+
Bridges page up to date: every step, the tool it waits on, any failure.
|
|
409
|
+
Needs `rhylthyme login` and rhylthyme-galago
|
|
410
|
+
(pip install "rhylthyme[galago]"). Only outbound HTTPS; tool addresses
|
|
411
|
+
never leave this machine. From the Bridges page you can pause, resume,
|
|
412
|
+
retry or skip a failed step and abort; this machine checks and logs each
|
|
413
|
+
command, and Ctrl-C here always wins.
|
|
414
|
+
"""
|
|
415
|
+
if program_file is None:
|
|
416
|
+
from .bridge_serve import serve
|
|
417
|
+
from .instruments import InstrumentSetupError
|
|
418
|
+
|
|
419
|
+
if live:
|
|
420
|
+
raise click.UsageError(
|
|
421
|
+
"--live applies to a PROGRAM_FILE run; to accept live runs "
|
|
422
|
+
"from the web, pass --allow-live."
|
|
423
|
+
)
|
|
424
|
+
try:
|
|
425
|
+
serve(
|
|
426
|
+
workcell,
|
|
427
|
+
schema_file=_default_schema_path(),
|
|
428
|
+
time_scale=time_scale,
|
|
429
|
+
record=not no_record,
|
|
430
|
+
allow_live=allow_live,
|
|
431
|
+
)
|
|
432
|
+
except InstrumentSetupError as e:
|
|
433
|
+
raise click.ClickException(str(e))
|
|
434
|
+
except KeyboardInterrupt:
|
|
435
|
+
click.echo("Bridge stopped.")
|
|
436
|
+
return
|
|
437
|
+
if allow_live:
|
|
438
|
+
raise click.UsageError(
|
|
439
|
+
"--allow-live is for waiting for runs from the web (no PROGRAM_FILE); "
|
|
440
|
+
"use --live to run PROGRAM_FILE on real hardware."
|
|
441
|
+
)
|
|
442
|
+
run_program(
|
|
443
|
+
program_file,
|
|
444
|
+
_default_schema_path(),
|
|
445
|
+
time_scale,
|
|
446
|
+
True,
|
|
447
|
+
False,
|
|
448
|
+
None,
|
|
449
|
+
record=not no_record,
|
|
450
|
+
factor_prompt=False,
|
|
451
|
+
workcell=workcell,
|
|
452
|
+
live=live,
|
|
453
|
+
confirm_live=confirm_live,
|
|
454
|
+
bridge=True,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
363
458
|
# Plan command
|
|
364
459
|
@cli.command()
|
|
365
460
|
@click.argument("input_file", type=click.Path(exists=True))
|
|
@@ -15,6 +15,7 @@ the events it emits are:
|
|
|
15
15
|
``program_resumed`` {time, wall_time, paused_seconds}
|
|
16
16
|
``program_aborted`` {time, reason} (reason goes to ``context.abortReason``)
|
|
17
17
|
``instrument_reply`` {step_id, time, ok, code, errorMessage, metadata}
|
|
18
|
+
``remote_command`` {time, id, kind, args, accepted, reason} (-> ``context.remoteCommands``)
|
|
18
19
|
|
|
19
20
|
All ``time`` values are the runner's program clock (an epoch float that
|
|
20
21
|
advances at ``time_scale``); the record stores them as seconds from the
|
|
@@ -298,6 +299,22 @@ class RunRecorder:
|
|
|
298
299
|
if data.get("metadata"):
|
|
299
300
|
reply["metadata"] = copy.deepcopy(data["metadata"])
|
|
300
301
|
log["replies"].append(reply)
|
|
302
|
+
elif event_type == "remote_command":
|
|
303
|
+
start = self._start_epoch()
|
|
304
|
+
self.context.setdefault("remoteCommands", []).append(
|
|
305
|
+
{
|
|
306
|
+
"id": data.get("id"),
|
|
307
|
+
"kind": data.get("kind"),
|
|
308
|
+
"args": copy.deepcopy(data.get("args") or {}),
|
|
309
|
+
"atSeconds": (
|
|
310
|
+
round(max(0.0, data["time"] - start), 3)
|
|
311
|
+
if start is not None and data.get("time") is not None
|
|
312
|
+
else None
|
|
313
|
+
),
|
|
314
|
+
"accepted": bool(data.get("accepted")),
|
|
315
|
+
"reason": data.get("reason") or "",
|
|
316
|
+
}
|
|
317
|
+
)
|
|
301
318
|
elif event_type == "program_aborted":
|
|
302
319
|
self.context["abortReason"] = data.get("reason") or "aborted"
|
|
303
320
|
elif event_type == "program_paused":
|
|
@@ -234,6 +234,90 @@ def open_instruments(
|
|
|
234
234
|
return InstrumentSession(galago.InstrumentExecutor(workcell, **kwargs), tools)
|
|
235
235
|
|
|
236
236
|
|
|
237
|
+
def start_bridge(
|
|
238
|
+
runner,
|
|
239
|
+
session: "InstrumentSession",
|
|
240
|
+
program: Mapping[str, Any],
|
|
241
|
+
*,
|
|
242
|
+
rest=None,
|
|
243
|
+
token_fn: Optional[Callable[[], str]] = None,
|
|
244
|
+
config_path=None,
|
|
245
|
+
program_id: Optional[str] = None,
|
|
246
|
+
allows_live: bool = False,
|
|
247
|
+
):
|
|
248
|
+
"""
|
|
249
|
+
Start publishing this run to rhylthyme.com (``rhylthyme bridge``): the
|
|
250
|
+
user's own ``bridges`` / ``bridge_state`` rows, owner-only. Needs
|
|
251
|
+
``rhylthyme login``. Returns the running Publisher; call ``stop()`` at the
|
|
252
|
+
end. Raises InstrumentSetupError if the site cannot be reached or written.
|
|
253
|
+
"""
|
|
254
|
+
import rhylthyme_galago
|
|
255
|
+
from rhylthyme_galago import bridge as B
|
|
256
|
+
|
|
257
|
+
from .remote import auth
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
if rest is None or token_fn is None:
|
|
261
|
+
creds = auth.load_credentials() or {}
|
|
262
|
+
if not creds.get("supabase_url"):
|
|
263
|
+
raise InstrumentSetupError(
|
|
264
|
+
"rhylthyme bridge needs you signed in: run `rhylthyme login`."
|
|
265
|
+
)
|
|
266
|
+
token_fn = token_fn or auth.access_token
|
|
267
|
+
rest = rest or B.SupabaseRest(
|
|
268
|
+
creds["supabase_url"], creds["supabase_anon_key"], token_fn
|
|
269
|
+
)
|
|
270
|
+
user_id = B.user_id_from_token(token_fn())
|
|
271
|
+
except (auth.AuthError, B.BridgeError) as e:
|
|
272
|
+
raise InstrumentSetupError(str(e)) from None
|
|
273
|
+
|
|
274
|
+
workcell = session.executor.workcell
|
|
275
|
+
bridge_id = B.bridge_id_for(
|
|
276
|
+
workcell, config_path or (auth.config_dir() / "bridges.json")
|
|
277
|
+
)
|
|
278
|
+
tools = [
|
|
279
|
+
{"name": c.tool, "type": workcell.tool(c.tool).type, "status": c.status.status}
|
|
280
|
+
for c in session.checks
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
def on_error(message: str) -> None:
|
|
284
|
+
runner.status_message = f"Bridge: {message}"
|
|
285
|
+
|
|
286
|
+
def submit(command: Dict[str, Any], wait: float = 3.0) -> Optional[Dict[str, Any]]:
|
|
287
|
+
"""Hand a browser command to the runner thread; wait for its decision."""
|
|
288
|
+
import json as _json
|
|
289
|
+
import time as _time
|
|
290
|
+
|
|
291
|
+
runner.command_queue.put("remote:" + _json.dumps(command))
|
|
292
|
+
deadline = _time.time() + wait
|
|
293
|
+
while _time.time() < deadline:
|
|
294
|
+
outcome = runner.remote_outcomes.get(command["id"])
|
|
295
|
+
if outcome is not None:
|
|
296
|
+
return outcome
|
|
297
|
+
_time.sleep(0.05)
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
publisher = B.Publisher(
|
|
301
|
+
runner,
|
|
302
|
+
rest=rest,
|
|
303
|
+
bridge_id=bridge_id,
|
|
304
|
+
user_id=user_id,
|
|
305
|
+
workcell=workcell,
|
|
306
|
+
tools=tools,
|
|
307
|
+
program=program,
|
|
308
|
+
mode="live" if session.live else "simulated",
|
|
309
|
+
allows_live=allows_live,
|
|
310
|
+
version=getattr(rhylthyme_galago, "__version__", ""),
|
|
311
|
+
on_error=on_error,
|
|
312
|
+
submit=submit,
|
|
313
|
+
program_id=program_id,
|
|
314
|
+
)
|
|
315
|
+
try:
|
|
316
|
+
return publisher.start()
|
|
317
|
+
except B.BridgeError as e:
|
|
318
|
+
raise InstrumentSetupError(f"Could not publish to rhylthyme.com: {e}") from None
|
|
319
|
+
|
|
320
|
+
|
|
237
321
|
def attach_instruments(
|
|
238
322
|
runner,
|
|
239
323
|
workcell_source,
|