rhylthyme-cli-runner 0.2.3a0__tar.gz → 0.2.5a0__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.
Files changed (87) hide show
  1. {rhylthyme_cli_runner-0.2.3a0/src/rhylthyme_cli_runner.egg-info → rhylthyme_cli_runner-0.2.5a0}/PKG-INFO +41 -8
  2. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/README.md +40 -7
  3. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/setup.py +1 -1
  4. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/__init__.py +1 -1
  5. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/cli.py +20 -0
  6. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/render.py +9 -0
  7. rhylthyme_cli_runner-0.2.5a0/src/rhylthyme_cli_runner/import_cli.py +262 -0
  8. rhylthyme_cli_runner-0.2.5a0/src/rhylthyme_cli_runner/render_cli.py +41 -0
  9. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0/src/rhylthyme_cli_runner.egg-info}/PKG-INFO +41 -8
  10. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner.egg-info/SOURCES.txt +4 -0
  11. rhylthyme_cli_runner-0.2.5a0/tests/test_import_cli.py +242 -0
  12. rhylthyme_cli_runner-0.2.5a0/tests/test_render_cli.py +33 -0
  13. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/LICENSE +0 -0
  14. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/pyproject.toml +0 -0
  15. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/setup.cfg +0 -0
  16. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/environment_icons.py +0 -0
  17. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/environment_loader.py +0 -0
  18. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/environment_schemas.py +0 -0
  19. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/__init__.py +0 -0
  20. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/cli.py +0 -0
  21. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/compare.py +0 -0
  22. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/gold.py +0 -0
  23. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/harness.py +0 -0
  24. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/llm.py +0 -0
  25. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/matcher.py +0 -0
  26. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/metrics.py +0 -0
  27. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/patterns/__init__.py +0 -0
  28. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/patterns/authoring_guide.md +0 -0
  29. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.md +0 -0
  30. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.py +0 -0
  31. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/patterns/extract.py +0 -0
  32. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/patterns/four_turn.py +0 -0
  33. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/eval/report.py +0 -0
  34. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/expand_replicates.py +0 -0
  35. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/__init__.py +0 -0
  36. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/calibrate.py +0 -0
  37. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/calibrate_cli.py +0 -0
  38. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/cli.py +0 -0
  39. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/evaluate.py +0 -0
  40. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/evaluate_cli.py +0 -0
  41. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/factors.py +0 -0
  42. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/hash.py +0 -0
  43. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/predict.py +0 -0
  44. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/recorder.py +0 -0
  45. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/replay.py +0 -0
  46. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/report.py +0 -0
  47. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/report_cli.py +0 -0
  48. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/store.py +0 -0
  49. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/synth.py +0 -0
  50. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/history/usable.py +0 -0
  51. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/instance_checks.py +0 -0
  52. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/program_planner.py +0 -0
  53. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/program_runner.py +0 -0
  54. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/remote/__init__.py +0 -0
  55. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/remote/auth.py +0 -0
  56. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/remote/checks.py +0 -0
  57. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/remote/cli.py +0 -0
  58. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/remote/mcp_client.py +0 -0
  59. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner/validate_program.py +0 -0
  60. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner.egg-info/dependency_links.txt +0 -0
  61. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner.egg-info/entry_points.txt +0 -0
  62. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner.egg-info/requires.txt +0 -0
  63. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/src/rhylthyme_cli_runner.egg-info/top_level.txt +0 -0
  64. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_calibrate.py +0 -0
  65. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_cli.py +0 -0
  66. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_eval_baseline.py +0 -0
  67. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_eval_harness.py +0 -0
  68. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_eval_providers.py +0 -0
  69. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_eval_scorer.py +0 -0
  70. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_evaluate.py +0 -0
  71. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_example_validation.py +0 -0
  72. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_examples_integration.py +0 -0
  73. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_expand_replicates.py +0 -0
  74. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_hash_parity.py +0 -0
  75. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_history_report.py +0 -0
  76. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_mcp_checks.py +0 -0
  77. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_planner_time_strings.py +0 -0
  78. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_predict.py +0 -0
  79. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_program_runner.py +0 -0
  80. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_remote.py +0 -0
  81. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_replay.py +0 -0
  82. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_run_recorder.py +0 -0
  83. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_skill.py +0 -0
  84. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_synth.py +0 -0
  85. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_usable.py +0 -0
  86. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/tests/test_validate_examples_ci.py +0 -0
  87. {rhylthyme_cli_runner-0.2.3a0 → rhylthyme_cli_runner-0.2.5a0}/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.2.3a0
3
+ Version: 0.2.5a0
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
@@ -64,7 +64,7 @@ Command-line interface for validating and running Rhylthyme real-time program sc
64
64
  and for turning a plain-language request into one through the hosted Rhylthyme MCP server.
65
65
 
66
66
  ```bash
67
- pip install rhylthyme-cli-runner
67
+ pip install rhylthyme
68
68
  rhylthyme login
69
69
  rhylthyme generate "roast chicken, potatoes and green beans for 6" -e kitchen --by 19:00 --with "one oven"
70
70
  ```
@@ -76,7 +76,7 @@ the oven, when do the balloons go up, and do the pizzas and the cake ever want
76
76
  the oven at the same time?
77
77
 
78
78
  ```bash
79
- pip install rhylthyme-cli-runner
79
+ pip install rhylthyme
80
80
 
81
81
  # 1. a program: the party, the kitchen and the setup crew, 13 steps
82
82
  curl -sO https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.json
@@ -117,8 +117,8 @@ Start Sat 12:50, finish Sat 16:00 (local time)
117
117
  # 4. a live timeline for the fridge door (or a phone): timers, cues, and you end the piñata when it breaks
118
118
  rhylthyme publish birthday-party.json --open --image party-preview.png
119
119
 
120
- # 5. a picture for the group chat, on the wall clock (needs Node; nothing to install)
121
- npx -y github:rhylthyme/rhylthyme-timeline birthday-party.json -o birthday-party.png \
120
+ # 5. a picture for the group chat, on the wall clock (needs Node.js)
121
+ rhylthyme render birthday-party.json -o birthday-party.png \
122
122
  --style web --palette vivid --label-overflow outside --start-at "$(date +%F)T12:50:00"
123
123
  ```
124
124
 
@@ -141,15 +141,16 @@ is the **server an AI assistant talks to**.
141
141
  |---|---|---|
142
142
  | What it is | The MCP **server**: the tools an AI assistant calls | A command-line **program**: the `rhylthyme` command |
143
143
  | Who uses it | Claude, ChatGPT, Cursor or any MCP client, on a person's behalf | A person at a terminal, a script, or CI |
144
- | Where it runs | Hosted at `mcp.rhylthyme.com`; nothing to install | On your machine: `pip install rhylthyme-cli-runner` |
144
+ | Where it runs | Hosted at `mcp.rhylthyme.com`; nothing to install | On your machine: `pip install rhylthyme` |
145
145
  | Language | JavaScript (Node 20+) | Python 3.12+ |
146
146
  | Input | A program the assistant builds in conversation | A program file on disk (JSON or YAML) |
147
147
  | Validate a program | `validate_program` | `rhylthyme validate` (works offline) |
148
+ | Import a recipe or protocol | `import_from_source` | `rhylthyme import` |
148
149
  | Timing, conflicts, deadlines | `analyze_schedule` | `rhylthyme analyze` (asks the server) |
149
150
  | Publish a live timeline | `visualize_schedule` | `rhylthyme publish` (asks the server) |
150
151
  | Run a schedule with timers | no: it hands back a link to the web timeline | `rhylthyme run`, an interactive terminal runner |
151
152
  | Recorded runs, calibration | reads runs saved to an account | records runs locally; `rhylthyme runs`, `rhylthyme calibrate` |
152
- | Catalog search, imports, account library | yes | no |
153
+ | Catalog search, account library | yes | no |
153
154
  | Also in the repository | the `rhylthyme-mcp` PyPI package (a stdio bridge to the hosted server), the Claude plugin marketplace | the Claude skill's source, the prompt-evaluation harness and its results, `rhylthyme mcp-test` |
154
155
 
155
156
  How they fit together: this tool is one of the MCP server's clients.
@@ -188,7 +189,8 @@ pip install -e ".[dev]"
188
189
  ### From PyPI
189
190
 
190
191
  ```bash
191
- pip install rhylthyme-cli-runner
192
+ pip install rhylthyme # this package, the importers and the renderer
193
+ pip install rhylthyme-cli-runner # this package alone
192
194
  ```
193
195
 
194
196
  ## Getting the Examples
@@ -542,6 +544,37 @@ A published timeline is reachable by anyone who has the link.
542
544
  - `--image PATH`: also save a PNG of the timeline
543
545
  - `--open`: open it in a browser
544
546
 
547
+ ### `rhylthyme import`
548
+
549
+ Imports a recipe, protocol or slide deck as a program, validates it, and
550
+ writes `<programId>.json`. Needs `rhylthyme-importers` (included in
551
+ `pip install rhylthyme`).
552
+
553
+ ```bash
554
+ rhylthyme import https://www.seriouseats.com/the-best-chili-recipe # any of ~580 recipe sites
555
+ rhylthyme import 52772 -i themealdb --publish # a TheMealDB id, straight to a live timeline
556
+ rhylthyme import https://www.protocols.io/view/western-blot-... # needs PROTOCOLS_IO_TOKEN
557
+ rhylthyme import https://raw.githubusercontent.com/Opentrons/Protocols/develop/protocols/007992/rna_isolation.ot2.apiv2.py # an Opentrons protocol
558
+ rhylthyme import "Neapolitan Pizza.cook" -i cooklang
559
+ rhylthyme importers # what is installed
560
+ rhylthyme search "pad thai" -i spoonacular # then import a hit's URL
561
+ ```
562
+
563
+ **Options:** `-i IMPORTER` (default: chosen from the URL), `-o PATH`,
564
+ `--stdout`, `--publish` (and `--open`), `--no-validate`. `-` reads pasted
565
+ source from stdin (with `-i`).
566
+
567
+ ### `rhylthyme render`
568
+
569
+ Draws a program as an SVG, PNG or PDF figure through the `rhylthyme-timeline`
570
+ package (included in `pip install rhylthyme`; needs Node.js 18+). Every
571
+ option of `rhylthyme-render` is accepted: `rhylthyme render --help`.
572
+
573
+ ```bash
574
+ rhylthyme render dinner.json -o dinner.png --style web --palette vivid --color-by task
575
+ rhylthyme render blot.json -o figure.pdf --style publication --legend right
576
+ ```
577
+
545
578
  ### `rhylthyme plan`
546
579
 
547
580
  An older stagger heuristic. It reads the pre-0.2 program format and leaves
@@ -4,7 +4,7 @@ Command-line interface for validating and running Rhylthyme real-time program sc
4
4
  and for turning a plain-language request into one through the hosted Rhylthyme MCP server.
5
5
 
6
6
  ```bash
7
- pip install rhylthyme-cli-runner
7
+ pip install rhylthyme
8
8
  rhylthyme login
9
9
  rhylthyme generate "roast chicken, potatoes and green beans for 6" -e kitchen --by 19:00 --with "one oven"
10
10
  ```
@@ -16,7 +16,7 @@ the oven, when do the balloons go up, and do the pizzas and the cake ever want
16
16
  the oven at the same time?
17
17
 
18
18
  ```bash
19
- pip install rhylthyme-cli-runner
19
+ pip install rhylthyme
20
20
 
21
21
  # 1. a program: the party, the kitchen and the setup crew, 13 steps
22
22
  curl -sO https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.json
@@ -57,8 +57,8 @@ Start Sat 12:50, finish Sat 16:00 (local time)
57
57
  # 4. a live timeline for the fridge door (or a phone): timers, cues, and you end the piñata when it breaks
58
58
  rhylthyme publish birthday-party.json --open --image party-preview.png
59
59
 
60
- # 5. a picture for the group chat, on the wall clock (needs Node; nothing to install)
61
- npx -y github:rhylthyme/rhylthyme-timeline birthday-party.json -o birthday-party.png \
60
+ # 5. a picture for the group chat, on the wall clock (needs Node.js)
61
+ rhylthyme render birthday-party.json -o birthday-party.png \
62
62
  --style web --palette vivid --label-overflow outside --start-at "$(date +%F)T12:50:00"
63
63
  ```
64
64
 
@@ -81,15 +81,16 @@ is the **server an AI assistant talks to**.
81
81
  |---|---|---|
82
82
  | What it is | The MCP **server**: the tools an AI assistant calls | A command-line **program**: the `rhylthyme` command |
83
83
  | Who uses it | Claude, ChatGPT, Cursor or any MCP client, on a person's behalf | A person at a terminal, a script, or CI |
84
- | Where it runs | Hosted at `mcp.rhylthyme.com`; nothing to install | On your machine: `pip install rhylthyme-cli-runner` |
84
+ | Where it runs | Hosted at `mcp.rhylthyme.com`; nothing to install | On your machine: `pip install rhylthyme` |
85
85
  | Language | JavaScript (Node 20+) | Python 3.12+ |
86
86
  | Input | A program the assistant builds in conversation | A program file on disk (JSON or YAML) |
87
87
  | Validate a program | `validate_program` | `rhylthyme validate` (works offline) |
88
+ | Import a recipe or protocol | `import_from_source` | `rhylthyme import` |
88
89
  | Timing, conflicts, deadlines | `analyze_schedule` | `rhylthyme analyze` (asks the server) |
89
90
  | Publish a live timeline | `visualize_schedule` | `rhylthyme publish` (asks the server) |
90
91
  | Run a schedule with timers | no: it hands back a link to the web timeline | `rhylthyme run`, an interactive terminal runner |
91
92
  | Recorded runs, calibration | reads runs saved to an account | records runs locally; `rhylthyme runs`, `rhylthyme calibrate` |
92
- | Catalog search, imports, account library | yes | no |
93
+ | Catalog search, account library | yes | no |
93
94
  | Also in the repository | the `rhylthyme-mcp` PyPI package (a stdio bridge to the hosted server), the Claude plugin marketplace | the Claude skill's source, the prompt-evaluation harness and its results, `rhylthyme mcp-test` |
94
95
 
95
96
  How they fit together: this tool is one of the MCP server's clients.
@@ -128,7 +129,8 @@ pip install -e ".[dev]"
128
129
  ### From PyPI
129
130
 
130
131
  ```bash
131
- pip install rhylthyme-cli-runner
132
+ pip install rhylthyme # this package, the importers and the renderer
133
+ pip install rhylthyme-cli-runner # this package alone
132
134
  ```
133
135
 
134
136
  ## Getting the Examples
@@ -482,6 +484,37 @@ A published timeline is reachable by anyone who has the link.
482
484
  - `--image PATH`: also save a PNG of the timeline
483
485
  - `--open`: open it in a browser
484
486
 
487
+ ### `rhylthyme import`
488
+
489
+ Imports a recipe, protocol or slide deck as a program, validates it, and
490
+ writes `<programId>.json`. Needs `rhylthyme-importers` (included in
491
+ `pip install rhylthyme`).
492
+
493
+ ```bash
494
+ rhylthyme import https://www.seriouseats.com/the-best-chili-recipe # any of ~580 recipe sites
495
+ rhylthyme import 52772 -i themealdb --publish # a TheMealDB id, straight to a live timeline
496
+ rhylthyme import https://www.protocols.io/view/western-blot-... # needs PROTOCOLS_IO_TOKEN
497
+ rhylthyme import https://raw.githubusercontent.com/Opentrons/Protocols/develop/protocols/007992/rna_isolation.ot2.apiv2.py # an Opentrons protocol
498
+ rhylthyme import "Neapolitan Pizza.cook" -i cooklang
499
+ rhylthyme importers # what is installed
500
+ rhylthyme search "pad thai" -i spoonacular # then import a hit's URL
501
+ ```
502
+
503
+ **Options:** `-i IMPORTER` (default: chosen from the URL), `-o PATH`,
504
+ `--stdout`, `--publish` (and `--open`), `--no-validate`. `-` reads pasted
505
+ source from stdin (with `-i`).
506
+
507
+ ### `rhylthyme render`
508
+
509
+ Draws a program as an SVG, PNG or PDF figure through the `rhylthyme-timeline`
510
+ package (included in `pip install rhylthyme`; needs Node.js 18+). Every
511
+ option of `rhylthyme-render` is accepted: `rhylthyme render --help`.
512
+
513
+ ```bash
514
+ rhylthyme render dinner.json -o dinner.png --style web --palette vivid --color-by task
515
+ rhylthyme render blot.json -o figure.pdf --style publication --legend right
516
+ ```
517
+
485
518
  ### `rhylthyme plan`
486
519
 
487
520
  An older stagger heuristic. It reads the pre-0.2 program format and leaves
@@ -19,7 +19,7 @@ def read_readme():
19
19
 
20
20
  setup(
21
21
  name="rhylthyme-cli-runner",
22
- version="0.2.3a0",
22
+ version="0.2.5a0",
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",
@@ -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.2.3a0"
9
+ __version__ = "0.2.5a0"
10
10
  __author__ = "Rhylthyme Team"
11
11
  __description__ = "CLI runner for Rhylthyme real-time program schedules"
12
12
 
@@ -544,6 +544,26 @@ def _register_remote_commands():
544
544
  _register_remote_commands()
545
545
 
546
546
 
547
+ def _register_import_commands():
548
+ """Attach `import`, `search`, `importers` (rhylthyme_cli_runner.import_cli)."""
549
+ from .import_cli import register as register_import
550
+
551
+ register_import(cli)
552
+
553
+
554
+ _register_import_commands()
555
+
556
+
557
+ def _register_render_command():
558
+ """Attach `render` (rhylthyme_cli_runner.render_cli)."""
559
+ from .render_cli import register as register_render
560
+
561
+ register_render(cli)
562
+
563
+
564
+ _register_render_command()
565
+
566
+
547
567
  def main():
548
568
  """Entry point for the CLI."""
549
569
  cli()
@@ -76,6 +76,15 @@ def find_renderer() -> Optional[Path]:
76
76
  candidate = Path(explicit).expanduser()
77
77
  # Absolute: `node -e` has no script directory to resolve against.
78
78
  return candidate.resolve() if candidate.is_file() else None
79
+ # The pip package (rhylthyme-timeline) carries the renderer.
80
+ try:
81
+ from rhylthyme_timeline import renderer_path
82
+
83
+ packaged = renderer_path()
84
+ if packaged.is_file():
85
+ return packaged
86
+ except ImportError:
87
+ pass
79
88
  here = Path(__file__).resolve()
80
89
  for parent in here.parents:
81
90
  for relative in _RENDERER_PATHS:
@@ -0,0 +1,262 @@
1
+ """``rhylthyme import`` and ``rhylthyme search``: the importers from
2
+ rhylthyme-importers, reached from the one command people already have.
3
+
4
+ ``rhylthyme-import`` (the importers' own command) still exists; this is the
5
+ same work with the CLI's validation and publishing on the end of it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sys
12
+ import tempfile
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ import click
17
+
18
+ INSTALL_HINT = (
19
+ "The importers are a separate package: pip install rhylthyme-importers "
20
+ "(or pip install rhylthyme, which includes them)."
21
+ )
22
+
23
+
24
+ def _registry():
25
+ try:
26
+ from rhylthyme_importers import ImporterRegistry
27
+ except ImportError:
28
+ raise click.ClickException(INSTALL_HINT)
29
+ return ImporterRegistry
30
+
31
+
32
+ def _pick(registry, source: str, importer_name: Optional[str]):
33
+ if importer_name:
34
+ importer = registry.get(importer_name)
35
+ if importer is None:
36
+ names = ", ".join(sorted(i["name"] for i in registry.list_importers()))
37
+ raise click.ClickException(
38
+ f"Unknown importer '{importer_name}'. Installed: {names}."
39
+ )
40
+ return importer
41
+ importer = registry.find_for_url(source)
42
+ if importer is None:
43
+ names = ", ".join(sorted(i["name"] for i in registry.list_importers()))
44
+ raise click.ClickException(
45
+ f"No importer recognises {source!r}. Pass -i with one of: {names}. "
46
+ "For a file on disk, name its importer (-i opentrons, -i cooklang, -i slidedeck)."
47
+ )
48
+ return importer
49
+
50
+
51
+ def _import_one(importer, source: str, text: Optional[str]):
52
+ # An importer that works on text (Opentrons) gets a URL's body fetched
53
+ # for it; the URL-native importers fetch for themselves.
54
+ url_native = bool(getattr(importer, "supported_domains", None))
55
+ if (
56
+ text is None
57
+ and source.startswith(("http://", "https://"))
58
+ and hasattr(importer, "import_from_source")
59
+ and not url_native
60
+ ):
61
+ from .remote.cli import _fetch_bytes
62
+
63
+ text = _fetch_bytes(source).decode("utf-8")
64
+ if text is not None and hasattr(importer, "import_from_source"):
65
+ return importer.import_from_source(text)
66
+ if text is not None and hasattr(importer, "import_from_content"):
67
+ return importer.import_from_content(
68
+ text, source_name=Path(source).stem if source != "-" else "recipe"
69
+ )
70
+ path = Path(source)
71
+ if path.is_file():
72
+ # A local file was named explicitly; that is the command line's job
73
+ # to allow (a server never does this).
74
+ if hasattr(importer, "allow_local_files"):
75
+ importer.allow_local_files = True
76
+ return importer.import_from_url(str(path))
77
+ return importer.import_from_url(source)
78
+
79
+
80
+ def _validation(program: dict):
81
+ from .validate_program import validate_program_file_structured
82
+
83
+ with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as tmp:
84
+ json.dump(program, tmp)
85
+ tmp_path = tmp.name
86
+ try:
87
+ from .cli import _default_schema_path
88
+
89
+ return validate_program_file_structured(
90
+ tmp_path, schema_file=_default_schema_path()
91
+ )
92
+ finally:
93
+ Path(tmp_path).unlink(missing_ok=True)
94
+
95
+
96
+ @click.command("import")
97
+ @click.argument("source", metavar="URL_OR_ID_OR_FILE")
98
+ @click.option(
99
+ "-i",
100
+ "--importer",
101
+ "importer_name",
102
+ default=None,
103
+ help="Which importer to use (default: chosen from the URL).",
104
+ )
105
+ @click.option(
106
+ "-o",
107
+ "--output",
108
+ type=click.Path(dir_okay=False),
109
+ default=None,
110
+ help="Write the program here (default: <name>.json in the current directory).",
111
+ )
112
+ @click.option(
113
+ "--stdout",
114
+ "to_stdout",
115
+ is_flag=True,
116
+ help="Print the program instead of writing a file.",
117
+ )
118
+ @click.option(
119
+ "--publish",
120
+ "do_publish",
121
+ is_flag=True,
122
+ help="Also publish it as a live timeline and print the URL.",
123
+ )
124
+ @click.option(
125
+ "--open",
126
+ "open_url",
127
+ is_flag=True,
128
+ help="With --publish: open the timeline in a browser.",
129
+ )
130
+ @click.option(
131
+ "--no-validate", is_flag=True, help="Skip validation of the imported program."
132
+ )
133
+ def import_command(
134
+ source, importer_name, output, to_stdout, do_publish, open_url, no_validate
135
+ ):
136
+ """Import a recipe, protocol or deck as a program.
137
+
138
+ \b
139
+ URL_OR_ID_OR_FILE is a recipe-site or protocols.io URL, a TheMealDB or
140
+ Spoonacular id, a .cook or GitHub .cook URL, or a local .py (Opentrons),
141
+ .cook or .pptx file. `-` reads the source text from stdin (with -i).
142
+ rhylthyme import https://www.seriouseats.com/the-best-chili-recipe
143
+ rhylthyme import 52772 -i themealdb --publish
144
+ rhylthyme import https://raw.githubusercontent.com/Opentrons/Protocols/develop/protocols/007992/rna_isolation.ot2.apiv2.py
145
+ rhylthyme importers # what is installed
146
+ """
147
+ registry = _registry()
148
+ text = None
149
+ if source == "-":
150
+ if not importer_name:
151
+ raise click.ClickException(
152
+ "Reading from stdin needs -i to say which importer."
153
+ )
154
+ text = sys.stdin.read()
155
+ importer = _pick(registry, source, importer_name)
156
+ click.echo(f"Importing with {importer.name}…", err=True)
157
+ result = _import_one(importer, source, text)
158
+ if not result.success:
159
+ raise click.ClickException(result.error or "import failed")
160
+ program = result.program
161
+
162
+ if not no_validate:
163
+ v = _validation(program)
164
+ errors = v.get("errors") or v.get("logic_errors") or []
165
+ if not v.get("is_valid", v.get("valid", True)):
166
+ for e in errors[:8]:
167
+ click.echo(
168
+ f" ✗ {e if isinstance(e, str) else e.get('message', e)}", err=True
169
+ )
170
+ raise click.ClickException(
171
+ "The imported program does not validate; not published. Re-run with --no-validate to keep it anyway."
172
+ )
173
+ for w in (v.get("warnings") or [])[:5]:
174
+ click.echo(
175
+ f" ! {w if isinstance(w, str) else w.get('message', w)}", err=True
176
+ )
177
+
178
+ payload = json.dumps(program, indent=2, ensure_ascii=False)
179
+ if to_stdout:
180
+ click.echo(payload)
181
+ else:
182
+ target = (
183
+ Path(output)
184
+ if output
185
+ else Path(f"{program.get('programId') or 'program'}.json")
186
+ )
187
+ target.write_text(payload + "\n")
188
+ click.echo(f"Saved {target}", err=True)
189
+
190
+ if do_publish:
191
+ from .remote.cli import publish as publish_command
192
+
193
+ ctx = click.get_current_context()
194
+ with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as tmp:
195
+ tmp.write(payload)
196
+ tmp_path = tmp.name
197
+ try:
198
+ ctx.invoke(
199
+ publish_command,
200
+ program_file=tmp_path,
201
+ environment=None,
202
+ open_url=open_url,
203
+ json_output=False,
204
+ quiet=True,
205
+ image_path=None,
206
+ )
207
+ finally:
208
+ Path(tmp_path).unlink(missing_ok=True)
209
+
210
+
211
+ @click.command("search")
212
+ @click.argument("query")
213
+ @click.option(
214
+ "-i",
215
+ "--importer",
216
+ "importer_name",
217
+ default="themealdb",
218
+ show_default=True,
219
+ help="Which catalogue to search.",
220
+ )
221
+ @click.option("--json", "json_output", is_flag=True)
222
+ def search_command(query, importer_name, json_output):
223
+ """Search a recipe or protocol source; import a hit with `rhylthyme import <url>`."""
224
+ registry = _registry()
225
+ importer = registry.get(importer_name)
226
+ if importer is None:
227
+ names = ", ".join(sorted(i["name"] for i in registry.list_importers()))
228
+ raise click.ClickException(
229
+ f"Unknown importer '{importer_name}'. Installed: {names}."
230
+ )
231
+ hits = importer.search(query)
232
+ if json_output:
233
+ click.echo(json.dumps(hits, indent=2, ensure_ascii=False))
234
+ return
235
+ if not hits:
236
+ click.echo("No results.")
237
+ return
238
+ for h in hits:
239
+ click.echo(f"{h.get('name')}\n {h.get('url')}")
240
+ if h.get("description"):
241
+ click.echo(f" {h['description']}")
242
+
243
+
244
+ @click.command("importers")
245
+ def importers_command():
246
+ """List the installed importers."""
247
+ registry = _registry()
248
+ for info in registry.list_importers():
249
+ domains = info.get("supported_domains") or []
250
+ where = (
251
+ ", ".join(domains[:4])
252
+ + (f", … ({len(domains)} sites)" if len(domains) > 4 else "")
253
+ if domains
254
+ else "files, ids or pasted text"
255
+ )
256
+ click.echo(f"{info['name']:18} {info['description']} [{where}]")
257
+
258
+
259
+ def register(cli_group: click.Group) -> None:
260
+ cli_group.add_command(import_command)
261
+ cli_group.add_command(search_command)
262
+ cli_group.add_command(importers_command)
@@ -0,0 +1,41 @@
1
+ """``rhylthyme render``: the timeline renderer from rhylthyme-timeline,
2
+ reached from the one command people already have. Every option of the
3
+ ``rhylthyme-render`` command is accepted unchanged."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import click
8
+
9
+ INSTALL_HINT = (
10
+ "The renderer is a separate package: pip install rhylthyme-timeline "
11
+ "(or pip install rhylthyme, which includes it). It runs on Node.js 18 or newer."
12
+ )
13
+
14
+
15
+ @click.command(
16
+ "render",
17
+ context_settings={
18
+ "ignore_unknown_options": True,
19
+ "allow_extra_args": True,
20
+ "help_option_names": [],
21
+ },
22
+ )
23
+ @click.argument("args", nargs=-1, type=click.UNPROCESSED)
24
+ @click.pass_context
25
+ def render_command(ctx, args):
26
+ """Draw a program as an SVG, PNG or PDF figure (needs Node.js).
27
+
28
+ \b
29
+ rhylthyme render dinner.json -o dinner.png --style web --palette vivid
30
+ rhylthyme render blot.json -o figure.pdf --style publication --legend right
31
+ rhylthyme render --help # every option
32
+ """
33
+ try:
34
+ from rhylthyme_timeline import main as render_main
35
+ except ImportError:
36
+ raise click.ClickException(INSTALL_HINT)
37
+ ctx.exit(render_main(list(args)))
38
+
39
+
40
+ def register(cli_group: click.Group) -> None:
41
+ cli_group.add_command(render_command)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rhylthyme-cli-runner
3
- Version: 0.2.3a0
3
+ Version: 0.2.5a0
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
@@ -64,7 +64,7 @@ Command-line interface for validating and running Rhylthyme real-time program sc
64
64
  and for turning a plain-language request into one through the hosted Rhylthyme MCP server.
65
65
 
66
66
  ```bash
67
- pip install rhylthyme-cli-runner
67
+ pip install rhylthyme
68
68
  rhylthyme login
69
69
  rhylthyme generate "roast chicken, potatoes and green beans for 6" -e kitchen --by 19:00 --with "one oven"
70
70
  ```
@@ -76,7 +76,7 @@ the oven, when do the balloons go up, and do the pizzas and the cake ever want
76
76
  the oven at the same time?
77
77
 
78
78
  ```bash
79
- pip install rhylthyme-cli-runner
79
+ pip install rhylthyme
80
80
 
81
81
  # 1. a program: the party, the kitchen and the setup crew, 13 steps
82
82
  curl -sO https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.json
@@ -117,8 +117,8 @@ Start Sat 12:50, finish Sat 16:00 (local time)
117
117
  # 4. a live timeline for the fridge door (or a phone): timers, cues, and you end the piñata when it breaks
118
118
  rhylthyme publish birthday-party.json --open --image party-preview.png
119
119
 
120
- # 5. a picture for the group chat, on the wall clock (needs Node; nothing to install)
121
- npx -y github:rhylthyme/rhylthyme-timeline birthday-party.json -o birthday-party.png \
120
+ # 5. a picture for the group chat, on the wall clock (needs Node.js)
121
+ rhylthyme render birthday-party.json -o birthday-party.png \
122
122
  --style web --palette vivid --label-overflow outside --start-at "$(date +%F)T12:50:00"
123
123
  ```
124
124
 
@@ -141,15 +141,16 @@ is the **server an AI assistant talks to**.
141
141
  |---|---|---|
142
142
  | What it is | The MCP **server**: the tools an AI assistant calls | A command-line **program**: the `rhylthyme` command |
143
143
  | Who uses it | Claude, ChatGPT, Cursor or any MCP client, on a person's behalf | A person at a terminal, a script, or CI |
144
- | Where it runs | Hosted at `mcp.rhylthyme.com`; nothing to install | On your machine: `pip install rhylthyme-cli-runner` |
144
+ | Where it runs | Hosted at `mcp.rhylthyme.com`; nothing to install | On your machine: `pip install rhylthyme` |
145
145
  | Language | JavaScript (Node 20+) | Python 3.12+ |
146
146
  | Input | A program the assistant builds in conversation | A program file on disk (JSON or YAML) |
147
147
  | Validate a program | `validate_program` | `rhylthyme validate` (works offline) |
148
+ | Import a recipe or protocol | `import_from_source` | `rhylthyme import` |
148
149
  | Timing, conflicts, deadlines | `analyze_schedule` | `rhylthyme analyze` (asks the server) |
149
150
  | Publish a live timeline | `visualize_schedule` | `rhylthyme publish` (asks the server) |
150
151
  | Run a schedule with timers | no: it hands back a link to the web timeline | `rhylthyme run`, an interactive terminal runner |
151
152
  | Recorded runs, calibration | reads runs saved to an account | records runs locally; `rhylthyme runs`, `rhylthyme calibrate` |
152
- | Catalog search, imports, account library | yes | no |
153
+ | Catalog search, account library | yes | no |
153
154
  | Also in the repository | the `rhylthyme-mcp` PyPI package (a stdio bridge to the hosted server), the Claude plugin marketplace | the Claude skill's source, the prompt-evaluation harness and its results, `rhylthyme mcp-test` |
154
155
 
155
156
  How they fit together: this tool is one of the MCP server's clients.
@@ -188,7 +189,8 @@ pip install -e ".[dev]"
188
189
  ### From PyPI
189
190
 
190
191
  ```bash
191
- pip install rhylthyme-cli-runner
192
+ pip install rhylthyme # this package, the importers and the renderer
193
+ pip install rhylthyme-cli-runner # this package alone
192
194
  ```
193
195
 
194
196
  ## Getting the Examples
@@ -542,6 +544,37 @@ A published timeline is reachable by anyone who has the link.
542
544
  - `--image PATH`: also save a PNG of the timeline
543
545
  - `--open`: open it in a browser
544
546
 
547
+ ### `rhylthyme import`
548
+
549
+ Imports a recipe, protocol or slide deck as a program, validates it, and
550
+ writes `<programId>.json`. Needs `rhylthyme-importers` (included in
551
+ `pip install rhylthyme`).
552
+
553
+ ```bash
554
+ rhylthyme import https://www.seriouseats.com/the-best-chili-recipe # any of ~580 recipe sites
555
+ rhylthyme import 52772 -i themealdb --publish # a TheMealDB id, straight to a live timeline
556
+ rhylthyme import https://www.protocols.io/view/western-blot-... # needs PROTOCOLS_IO_TOKEN
557
+ rhylthyme import https://raw.githubusercontent.com/Opentrons/Protocols/develop/protocols/007992/rna_isolation.ot2.apiv2.py # an Opentrons protocol
558
+ rhylthyme import "Neapolitan Pizza.cook" -i cooklang
559
+ rhylthyme importers # what is installed
560
+ rhylthyme search "pad thai" -i spoonacular # then import a hit's URL
561
+ ```
562
+
563
+ **Options:** `-i IMPORTER` (default: chosen from the URL), `-o PATH`,
564
+ `--stdout`, `--publish` (and `--open`), `--no-validate`. `-` reads pasted
565
+ source from stdin (with `-i`).
566
+
567
+ ### `rhylthyme render`
568
+
569
+ Draws a program as an SVG, PNG or PDF figure through the `rhylthyme-timeline`
570
+ package (included in `pip install rhylthyme`; needs Node.js 18+). Every
571
+ option of `rhylthyme-render` is accepted: `rhylthyme render --help`.
572
+
573
+ ```bash
574
+ rhylthyme render dinner.json -o dinner.png --style web --palette vivid --color-by task
575
+ rhylthyme render blot.json -o figure.pdf --style publication --legend right
576
+ ```
577
+
545
578
  ### `rhylthyme plan`
546
579
 
547
580
  An older stagger heuristic. It reads the pre-0.2 program format and leaves
@@ -8,9 +8,11 @@ src/rhylthyme_cli_runner/environment_icons.py
8
8
  src/rhylthyme_cli_runner/environment_loader.py
9
9
  src/rhylthyme_cli_runner/environment_schemas.py
10
10
  src/rhylthyme_cli_runner/expand_replicates.py
11
+ src/rhylthyme_cli_runner/import_cli.py
11
12
  src/rhylthyme_cli_runner/instance_checks.py
12
13
  src/rhylthyme_cli_runner/program_planner.py
13
14
  src/rhylthyme_cli_runner/program_runner.py
15
+ src/rhylthyme_cli_runner/render_cli.py
14
16
  src/rhylthyme_cli_runner/validate_program.py
15
17
  src/rhylthyme_cli_runner.egg-info/PKG-INFO
16
18
  src/rhylthyme_cli_runner.egg-info/SOURCES.txt
@@ -67,11 +69,13 @@ tests/test_examples_integration.py
67
69
  tests/test_expand_replicates.py
68
70
  tests/test_hash_parity.py
69
71
  tests/test_history_report.py
72
+ tests/test_import_cli.py
70
73
  tests/test_mcp_checks.py
71
74
  tests/test_planner_time_strings.py
72
75
  tests/test_predict.py
73
76
  tests/test_program_runner.py
74
77
  tests/test_remote.py
78
+ tests/test_render_cli.py
75
79
  tests/test_replay.py
76
80
  tests/test_run_recorder.py
77
81
  tests/test_skill.py
@@ -0,0 +1,242 @@
1
+ """`rhylthyme import`, `search` and `importers`, with a fake importer registry
2
+ so nothing is fetched."""
3
+
4
+ import json
5
+ import sys
6
+ import types
7
+
8
+ import pytest
9
+ from click.testing import CliRunner
10
+
11
+ from rhylthyme_cli_runner.cli import cli
12
+
13
+ PROGRAM = {
14
+ "schemaVersion": "0.3.0-alpha",
15
+ "programId": "fake-dinner",
16
+ "name": "Fake dinner",
17
+ "environmentType": "kitchen",
18
+ "tracks": [
19
+ {
20
+ "trackId": "a",
21
+ "name": "A",
22
+ "steps": [
23
+ {
24
+ "stepId": "s1",
25
+ "name": "Cook",
26
+ "task": "stove",
27
+ "duration": {"type": "fixed", "seconds": 600},
28
+ "startTrigger": {"type": "programStart"},
29
+ }
30
+ ],
31
+ }
32
+ ],
33
+ "resourceConstraints": [{"task": "stove", "maxConcurrent": 1}],
34
+ }
35
+ BAD = dict(
36
+ PROGRAM,
37
+ tracks=[
38
+ {
39
+ "trackId": "a",
40
+ "name": "A",
41
+ "steps": [
42
+ {
43
+ "stepId": "s1",
44
+ "name": "Cook",
45
+ "task": "stove",
46
+ "duration": {"type": "fixed", "seconds": 600},
47
+ "startTrigger": {"type": "afterStep", "stepId": "nope"},
48
+ }
49
+ ],
50
+ }
51
+ ],
52
+ )
53
+
54
+
55
+ class Result:
56
+ def __init__(self, program=None, error=None):
57
+ self.success = program is not None
58
+ self.program = program
59
+ self.error = error
60
+
61
+
62
+ class FakeImporter:
63
+ name = "fake"
64
+ description = "Fake source"
65
+ supported_domains = ["fake.example"]
66
+ allow_local_files = False
67
+
68
+ def __init__(self):
69
+ self.calls = []
70
+
71
+ def can_import(self, s):
72
+ return "fake.example" in s
73
+
74
+ def search(self, q):
75
+ return [
76
+ {
77
+ "name": f"Hit for {q}",
78
+ "url": "https://fake.example/1",
79
+ "description": "d",
80
+ }
81
+ ]
82
+
83
+ def import_from_url(self, url):
84
+ self.calls.append(("url", url, self.allow_local_files))
85
+ return Result(BAD if "bad" in url else PROGRAM)
86
+
87
+ def import_from_source(self, text):
88
+ self.calls.append(("text", text, None))
89
+ return Result(PROGRAM)
90
+
91
+
92
+ @pytest.fixture
93
+ def fake(monkeypatch):
94
+ importer = FakeImporter()
95
+
96
+ class Registry:
97
+ @staticmethod
98
+ def get(name):
99
+ return importer if name == "fake" else None
100
+
101
+ @staticmethod
102
+ def find_for_url(url):
103
+ return importer if importer.can_import(url) else None
104
+
105
+ @staticmethod
106
+ def list_importers():
107
+ return [
108
+ {
109
+ "name": "fake",
110
+ "description": "Fake source",
111
+ "supported_domains": ["fake.example"],
112
+ }
113
+ ]
114
+
115
+ module = types.ModuleType("rhylthyme_importers")
116
+ module.ImporterRegistry = Registry
117
+ monkeypatch.setitem(sys.modules, "rhylthyme_importers", module)
118
+ return importer
119
+
120
+
121
+ def test_import_by_url_writes_a_named_file(fake, tmp_path, monkeypatch):
122
+ monkeypatch.chdir(tmp_path)
123
+ r = CliRunner().invoke(cli, ["import", "https://fake.example/dinner"])
124
+ assert r.exit_code == 0, r.output
125
+ assert (
126
+ json.loads((tmp_path / "fake-dinner.json").read_text())["name"] == "Fake dinner"
127
+ )
128
+ assert "Importing with fake" in r.output
129
+
130
+
131
+ def test_explicit_importer_output_path_and_stdout(fake, tmp_path):
132
+ out = tmp_path / "d.json"
133
+ r = CliRunner().invoke(cli, ["import", "12345", "-i", "fake", "-o", str(out)])
134
+ assert (
135
+ r.exit_code == 0 and json.loads(out.read_text())["programId"] == "fake-dinner"
136
+ )
137
+ r = CliRunner().invoke(cli, ["import", "12345", "-i", "fake", "--stdout"])
138
+ assert r.exit_code == 0 and '"programId": "fake-dinner"' in r.output
139
+
140
+
141
+ def test_a_local_file_is_allowed_from_the_command_line(fake, tmp_path):
142
+ f = tmp_path / "protocol.py"
143
+ f.write_text("def run(protocol): pass\n")
144
+ r = CliRunner().invoke(cli, ["import", str(f), "-i", "fake", "--stdout"])
145
+ assert r.exit_code == 0, r.output
146
+ assert fake.calls[-1] == (
147
+ "url",
148
+ str(f),
149
+ True,
150
+ ), "the CLI opts in to local files; servers never do"
151
+
152
+
153
+ def test_stdin_needs_an_importer_and_goes_to_import_from_source(fake):
154
+ r = CliRunner().invoke(cli, ["import", "-"], input="text")
155
+ assert r.exit_code != 0 and "-i" in r.output
156
+ r = CliRunner().invoke(
157
+ cli, ["import", "-", "-i", "fake", "--stdout"], input="def run(p): pass"
158
+ )
159
+ assert r.exit_code == 0 and fake.calls[-1][0] == "text"
160
+
161
+
162
+ def test_an_invalid_import_is_reported_and_not_published(fake, tmp_path):
163
+ r = CliRunner().invoke(
164
+ cli,
165
+ [
166
+ "import",
167
+ "https://fake.example/bad",
168
+ "-o",
169
+ str(tmp_path / "x.json"),
170
+ "--publish",
171
+ ],
172
+ )
173
+ assert r.exit_code != 0
174
+ assert "does not validate" in r.output and "nope" in r.output
175
+ assert not (tmp_path / "x.json").exists()
176
+
177
+
178
+ def test_publish_after_import(fake, tmp_path, monkeypatch):
179
+ from rhylthyme_cli_runner.remote import cli as remote
180
+
181
+ seen = {}
182
+
183
+ def fake_publish(
184
+ program_file, environment, open_url, json_output, quiet, image_path
185
+ ):
186
+ seen["program"] = json.loads(open(program_file).read())
187
+ import click
188
+
189
+ click.echo("https://kitchen.rhylthyme.com?share=abc")
190
+
191
+ monkeypatch.setattr(remote.publish, "callback", fake_publish)
192
+ r = CliRunner().invoke(
193
+ cli,
194
+ [
195
+ "import",
196
+ "https://fake.example/dinner",
197
+ "-o",
198
+ str(tmp_path / "d.json"),
199
+ "--publish",
200
+ ],
201
+ )
202
+ assert r.exit_code == 0, r.output
203
+ assert seen["program"]["programId"] == "fake-dinner"
204
+ assert "share=abc" in r.output
205
+
206
+
207
+ def test_unknown_importer_and_unrecognised_source(fake):
208
+ r = CliRunner().invoke(cli, ["import", "x", "-i", "nope"])
209
+ assert r.exit_code != 0 and "Unknown importer" in r.output and "fake" in r.output
210
+ r = CliRunner().invoke(cli, ["import", "https://example.org/x"])
211
+ assert r.exit_code != 0 and "No importer recognises" in r.output
212
+
213
+
214
+ def test_search_and_importers(fake):
215
+ r = CliRunner().invoke(cli, ["search", "chili", "-i", "fake"])
216
+ assert (
217
+ r.exit_code == 0
218
+ and "Hit for chili" in r.output
219
+ and "https://fake.example/1" in r.output
220
+ )
221
+ r = CliRunner().invoke(cli, ["importers"])
222
+ assert r.exit_code == 0 and "fake" in r.output and "fake.example" in r.output
223
+
224
+
225
+ def test_without_the_importers_package_the_message_says_how_to_get_it(monkeypatch):
226
+ monkeypatch.setitem(sys.modules, "rhylthyme_importers", None)
227
+ r = CliRunner().invoke(cli, ["import", "https://fake.example/dinner"])
228
+ assert r.exit_code != 0 and "pip install rhylthyme-importers" in r.output
229
+
230
+
231
+ def test_a_text_importer_gets_a_urls_body(fake, monkeypatch):
232
+ from rhylthyme_cli_runner.remote import cli as remote
233
+
234
+ monkeypatch.setattr(
235
+ remote, "_fetch_bytes", lambda url, limit=0: b"def run(protocol): pass\n"
236
+ )
237
+ fake.supported_domains = [] # a file/text importer, like Opentrons
238
+ r = CliRunner().invoke(
239
+ cli, ["import", "https://fake.example/protocol.py", "-i", "fake", "--stdout"]
240
+ )
241
+ assert r.exit_code == 0, r.output
242
+ assert fake.calls[-1] == ("text", "def run(protocol): pass\n", None)
@@ -0,0 +1,33 @@
1
+ """`rhylthyme render` hands its arguments to the rhylthyme-timeline package."""
2
+
3
+ import sys
4
+ import types
5
+
6
+ from click.testing import CliRunner
7
+
8
+ from rhylthyme_cli_runner.cli import cli
9
+
10
+
11
+ def test_arguments_pass_through_unchanged(monkeypatch):
12
+ seen = {}
13
+ module = types.ModuleType("rhylthyme_timeline")
14
+ module.main = lambda argv: seen.setdefault("argv", argv) and 0
15
+ monkeypatch.setitem(sys.modules, "rhylthyme_timeline", module)
16
+ r = CliRunner().invoke(
17
+ cli, ["render", "p.json", "-o", "p.png", "--style", "web", "--help"]
18
+ )
19
+ assert r.exit_code == 0, r.output
20
+ assert seen["argv"] == ["p.json", "-o", "p.png", "--style", "web", "--help"]
21
+
22
+
23
+ def test_exit_code_is_the_renderers(monkeypatch):
24
+ module = types.ModuleType("rhylthyme_timeline")
25
+ module.main = lambda argv: 2
26
+ monkeypatch.setitem(sys.modules, "rhylthyme_timeline", module)
27
+ assert CliRunner().invoke(cli, ["render", "p.json"]).exit_code == 2
28
+
29
+
30
+ def test_without_the_package_the_message_says_how_to_get_it(monkeypatch):
31
+ monkeypatch.setitem(sys.modules, "rhylthyme_timeline", None)
32
+ r = CliRunner().invoke(cli, ["render", "p.json"])
33
+ assert r.exit_code != 0 and "pip install rhylthyme-timeline" in r.output