rigbox 0.1.0__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.
- rigbox-0.1.0/LICENSE +21 -0
- rigbox-0.1.0/PKG-INFO +99 -0
- rigbox-0.1.0/README.md +68 -0
- rigbox-0.1.0/pyproject.toml +71 -0
- rigbox-0.1.0/setup.cfg +4 -0
- rigbox-0.1.0/src/rigbox/__init__.py +1 -0
- rigbox-0.1.0/src/rigbox/cli.py +142 -0
- rigbox-0.1.0/src/rigbox/constants.py +89 -0
- rigbox-0.1.0/src/rigbox/core/__init__.py +1 -0
- rigbox-0.1.0/src/rigbox/core/cli_parser.py +145 -0
- rigbox-0.1.0/src/rigbox/core/context.py +129 -0
- rigbox-0.1.0/src/rigbox/core/history.py +44 -0
- rigbox-0.1.0/src/rigbox/core/plugins.py +76 -0
- rigbox-0.1.0/src/rigbox/core/registry.py +191 -0
- rigbox-0.1.0/src/rigbox/core/render.py +253 -0
- rigbox-0.1.0/src/rigbox/core/transport.py +78 -0
- rigbox-0.1.0/src/rigbox/core/workflow.py +94 -0
- rigbox-0.1.0/src/rigbox/core/workspace.py +96 -0
- rigbox-0.1.0/src/rigbox/hooks.py +30 -0
- rigbox-0.1.0/src/rigbox/plugins/__init__.py +1 -0
- rigbox-0.1.0/src/rigbox/plugins/builtin.py +130 -0
- rigbox-0.1.0/src/rigbox/plugins/code/__init__.py +1 -0
- rigbox-0.1.0/src/rigbox/plugins/code/commands.py +84 -0
- rigbox-0.1.0/src/rigbox/plugins/code/generator.py +154 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/__init__.py +0 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_autogen_banner.j2 +27 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_autogen_banner_editable.j2 +27 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_autogen_banner_state_machine.j2 +29 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_enum.j2 +10 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_actions.j2 +7 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_constants.j2 +5 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_errors.j2 +10 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_events.j2 +10 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_state_table_entry.j2 +20 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_state_transition_table.j2 +20 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_states.j2 +9 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_struct.j2 +6 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_version.j2 +3 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_struct.j2 +8 -0
- rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_union.j2 +8 -0
- rigbox-0.1.0/src/rigbox/plugins/code/render.py +106 -0
- rigbox-0.1.0/src/rigbox/plugins/code/template.py +64 -0
- rigbox-0.1.0/src/rigbox/plugins/code/templates/c/state_machine.h.j2 +42 -0
- rigbox-0.1.0/src/rigbox/plugins/device/__init__.py +1 -0
- rigbox-0.1.0/src/rigbox/plugins/device/commands.py +236 -0
- rigbox-0.1.0/src/rigbox/plugins/device/connection.py +160 -0
- rigbox-0.1.0/src/rigbox/plugins/device/replay_transport.py +77 -0
- rigbox-0.1.0/src/rigbox/plugins/device/serial_transport.py +84 -0
- rigbox-0.1.0/src/rigbox/plugins/manifests/__init__.py +1 -0
- rigbox-0.1.0/src/rigbox/plugins/manifests/api.py +96 -0
- rigbox-0.1.0/src/rigbox/plugins/manifests/build.py +98 -0
- rigbox-0.1.0/src/rigbox/plugins/manifests/check.py +79 -0
- rigbox-0.1.0/src/rigbox/plugins/manifests/commands.py +98 -0
- rigbox-0.1.0/src/rigbox/plugins/manifests/schema.py +130 -0
- rigbox-0.1.0/src/rigbox/plugins/plugin_template.py +43 -0
- rigbox-0.1.0/src/rigbox/plugins/statemachine/__init__.py +1 -0
- rigbox-0.1.0/src/rigbox/plugins/statemachine/check.py +169 -0
- rigbox-0.1.0/src/rigbox/plugins/statemachine/commands.py +357 -0
- rigbox-0.1.0/src/rigbox/plugins/statemachine/coverage.py +156 -0
- rigbox-0.1.0/src/rigbox/plugins/statemachine/engine.py +186 -0
- rigbox-0.1.0/src/rigbox/plugins/statemachine/fuzz.py +181 -0
- rigbox-0.1.0/src/rigbox/plugins/statemachine/schema.py +241 -0
- rigbox-0.1.0/src/rigbox/repl.py +135 -0
- rigbox-0.1.0/src/rigbox.egg-info/PKG-INFO +99 -0
- rigbox-0.1.0/src/rigbox.egg-info/SOURCES.txt +81 -0
- rigbox-0.1.0/src/rigbox.egg-info/dependency_links.txt +1 -0
- rigbox-0.1.0/src/rigbox.egg-info/entry_points.txt +2 -0
- rigbox-0.1.0/src/rigbox.egg-info/requires.txt +23 -0
- rigbox-0.1.0/src/rigbox.egg-info/top_level.txt +1 -0
- rigbox-0.1.0/tests/test_cli_parser.py +217 -0
- rigbox-0.1.0/tests/test_codegen.py +180 -0
- rigbox-0.1.0/tests/test_core.py +263 -0
- rigbox-0.1.0/tests/test_device.py +167 -0
- rigbox-0.1.0/tests/test_device_session.py +350 -0
- rigbox-0.1.0/tests/test_dsp.py +436 -0
- rigbox-0.1.0/tests/test_manifests.py +157 -0
- rigbox-0.1.0/tests/test_plugin_new.py +66 -0
- rigbox-0.1.0/tests/test_render.py +138 -0
- rigbox-0.1.0/tests/test_shell.py +150 -0
- rigbox-0.1.0/tests/test_sm_checks.py +198 -0
- rigbox-0.1.0/tests/test_sm_fuzz.py +190 -0
- rigbox-0.1.0/tests/test_statemachine.py +366 -0
- rigbox-0.1.0/tests/test_transports.py +186 -0
rigbox-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 parsimonix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rigbox-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rigbox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Embedded developer's toolbox: a manifest-driven REPL and CLI.
|
|
5
|
+
Author-email: Jonathan Bailey <jonathan.bailey@comcast.net>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: cmd2<5,>=4.3
|
|
10
|
+
Requires-Dist: construct>=2.10
|
|
11
|
+
Requires-Dist: jinja2>=3.1.6
|
|
12
|
+
Requires-Dist: networkx<4,>=3.4
|
|
13
|
+
Requires-Dist: numpy>=1.24
|
|
14
|
+
Requires-Dist: pluggy>=1.3
|
|
15
|
+
Requires-Dist: pydantic>=2
|
|
16
|
+
Requires-Dist: pyserial>=3.5
|
|
17
|
+
Requires-Dist: pyyaml>=6
|
|
18
|
+
Requires-Dist: rich>=15
|
|
19
|
+
Requires-Dist: scipy>=1.10
|
|
20
|
+
Requires-Dist: simpleeval>=1.0
|
|
21
|
+
Requires-Dist: transitions>=0.9
|
|
22
|
+
Requires-Dist: typer>=0.27.3
|
|
23
|
+
Provides-Extra: plot
|
|
24
|
+
Requires-Dist: matplotlib>=3.7; extra == "plot"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
27
|
+
Requires-Dist: matplotlib>=3.7; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
29
|
+
Requires-Dist: ty==0.0.89; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# rigbox
|
|
33
|
+
|
|
34
|
+
A manifest-driven REPL and CLI toolbox for embedded and DSP developers.
|
|
35
|
+
|
|
36
|
+
## Setup
|
|
37
|
+
|
|
38
|
+
Requires Python 3.11 or newer.
|
|
39
|
+
|
|
40
|
+
```shell
|
|
41
|
+
python -m venv .venv
|
|
42
|
+
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
|
|
43
|
+
.\.venv\Scripts\python.exe -m pytest -q
|
|
44
|
+
.\.venv\Scripts\ruff.exe check # lint
|
|
45
|
+
.\.venv\Scripts\ty.exe check # type check
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Use
|
|
49
|
+
|
|
50
|
+
```shell
|
|
51
|
+
rigbox init my-project # create a workspace (rigbox.yaml)
|
|
52
|
+
rigbox # interactive shell (run from inside a workspace)
|
|
53
|
+
rigbox run commands # list the commands
|
|
54
|
+
rigbox run commands --help # one command's arguments
|
|
55
|
+
rigbox run --format json commands
|
|
56
|
+
rigbox script workflows\x.yaml --var key=value
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Output is a human message (or a table for data-only commands) on a terminal and NDJSON when piped.
|
|
60
|
+
Select `--format plaintext`, `rich`, `json`, or `ndjson` explicitly when needed.
|
|
61
|
+
Human formats fall back to available text or a table of structured data; plaintext output omits styling.
|
|
62
|
+
JSON and NDJSON never fall back to human text: human-only results produce an explicit error.
|
|
63
|
+
Empty results produce `[]` in JSON or no NDJSON records, without warnings.
|
|
64
|
+
|
|
65
|
+
### The shell
|
|
66
|
+
|
|
67
|
+
The shell is a [cmd2](https://github.com/python-cmd2/cmd2) app; every rigbox command is a shell command with the same
|
|
68
|
+
arguments and `--help` as `rigbox run`. Tab completes command names, option names, choices and file names. cmd2's own
|
|
69
|
+
commands work too:
|
|
70
|
+
|
|
71
|
+
```shell
|
|
72
|
+
rigbox> help # commands grouped by area; `help commands` or `commands --help` for one
|
|
73
|
+
rigbox> set format json # print results as JSON (plaintext, rich, json, ndjson, auto)
|
|
74
|
+
rigbox> alias create list commands # then: list
|
|
75
|
+
rigbox> commands | findstr workspace # pipe to a shell command
|
|
76
|
+
rigbox> commands > commands.txt # redirect to a file
|
|
77
|
+
rigbox> history # what you typed (kept in .rigbox/ inside a workspace)
|
|
78
|
+
rigbox> exit # or quit, or Ctrl-D
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The bar under the prompt shows the workspace, open device connections and output format.
|
|
82
|
+
|
|
83
|
+
## Plugins
|
|
84
|
+
|
|
85
|
+
See each plugin's README for commands, examples and behavior:
|
|
86
|
+
|
|
87
|
+
- [Manifests](src/rigbox/plugins/manifests/README.md)
|
|
88
|
+
- [Devices and transports](src/rigbox/plugins/device/README.md)
|
|
89
|
+
- [State machines](src/rigbox/plugins/statemachine/README.md)
|
|
90
|
+
- [Code generation](src/rigbox/plugins/code/README.md)
|
|
91
|
+
- [DSP](src/rigbox/plugins/dsp/README.md)
|
|
92
|
+
- [Built-in utilities and adding plugins](src/rigbox/plugins/README.md)
|
|
93
|
+
|
|
94
|
+
## Layout
|
|
95
|
+
|
|
96
|
+
- `src/rigbox/core/`: framework (registry, parsing, rendering, workspace, workflows, plugin loading)
|
|
97
|
+
- `src/rigbox/plugins/`: built-in plugins
|
|
98
|
+
- `docs/decisions.md`: architecture decisions and rationale
|
|
99
|
+
- `CLAUDE.md`: conventions and preferences for contributors and Claude Code
|
rigbox-0.1.0/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# rigbox
|
|
2
|
+
|
|
3
|
+
A manifest-driven REPL and CLI toolbox for embedded and DSP developers.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Requires Python 3.11 or newer.
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
python -m venv .venv
|
|
11
|
+
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
|
|
12
|
+
.\.venv\Scripts\python.exe -m pytest -q
|
|
13
|
+
.\.venv\Scripts\ruff.exe check # lint
|
|
14
|
+
.\.venv\Scripts\ty.exe check # type check
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Use
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
rigbox init my-project # create a workspace (rigbox.yaml)
|
|
21
|
+
rigbox # interactive shell (run from inside a workspace)
|
|
22
|
+
rigbox run commands # list the commands
|
|
23
|
+
rigbox run commands --help # one command's arguments
|
|
24
|
+
rigbox run --format json commands
|
|
25
|
+
rigbox script workflows\x.yaml --var key=value
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Output is a human message (or a table for data-only commands) on a terminal and NDJSON when piped.
|
|
29
|
+
Select `--format plaintext`, `rich`, `json`, or `ndjson` explicitly when needed.
|
|
30
|
+
Human formats fall back to available text or a table of structured data; plaintext output omits styling.
|
|
31
|
+
JSON and NDJSON never fall back to human text: human-only results produce an explicit error.
|
|
32
|
+
Empty results produce `[]` in JSON or no NDJSON records, without warnings.
|
|
33
|
+
|
|
34
|
+
### The shell
|
|
35
|
+
|
|
36
|
+
The shell is a [cmd2](https://github.com/python-cmd2/cmd2) app; every rigbox command is a shell command with the same
|
|
37
|
+
arguments and `--help` as `rigbox run`. Tab completes command names, option names, choices and file names. cmd2's own
|
|
38
|
+
commands work too:
|
|
39
|
+
|
|
40
|
+
```shell
|
|
41
|
+
rigbox> help # commands grouped by area; `help commands` or `commands --help` for one
|
|
42
|
+
rigbox> set format json # print results as JSON (plaintext, rich, json, ndjson, auto)
|
|
43
|
+
rigbox> alias create list commands # then: list
|
|
44
|
+
rigbox> commands | findstr workspace # pipe to a shell command
|
|
45
|
+
rigbox> commands > commands.txt # redirect to a file
|
|
46
|
+
rigbox> history # what you typed (kept in .rigbox/ inside a workspace)
|
|
47
|
+
rigbox> exit # or quit, or Ctrl-D
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The bar under the prompt shows the workspace, open device connections and output format.
|
|
51
|
+
|
|
52
|
+
## Plugins
|
|
53
|
+
|
|
54
|
+
See each plugin's README for commands, examples and behavior:
|
|
55
|
+
|
|
56
|
+
- [Manifests](src/rigbox/plugins/manifests/README.md)
|
|
57
|
+
- [Devices and transports](src/rigbox/plugins/device/README.md)
|
|
58
|
+
- [State machines](src/rigbox/plugins/statemachine/README.md)
|
|
59
|
+
- [Code generation](src/rigbox/plugins/code/README.md)
|
|
60
|
+
- [DSP](src/rigbox/plugins/dsp/README.md)
|
|
61
|
+
- [Built-in utilities and adding plugins](src/rigbox/plugins/README.md)
|
|
62
|
+
|
|
63
|
+
## Layout
|
|
64
|
+
|
|
65
|
+
- `src/rigbox/core/`: framework (registry, parsing, rendering, workspace, workflows, plugin loading)
|
|
66
|
+
- `src/rigbox/plugins/`: built-in plugins
|
|
67
|
+
- `docs/decisions.md`: architecture decisions and rationale
|
|
68
|
+
- `CLAUDE.md`: conventions and preferences for contributors and Claude Code
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rigbox"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Embedded developer's toolbox: a manifest-driven REPL and CLI."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Jonathan Bailey", email = "jonathan.bailey@comcast.net" }
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"cmd2>=4.3,<5",
|
|
16
|
+
"construct>=2.10",
|
|
17
|
+
"jinja2>=3.1.6",
|
|
18
|
+
"networkx>=3.4,<4",
|
|
19
|
+
"numpy>=1.24",
|
|
20
|
+
"pluggy>=1.3",
|
|
21
|
+
"pydantic>=2",
|
|
22
|
+
"pyserial>=3.5",
|
|
23
|
+
"pyyaml>=6",
|
|
24
|
+
"rich>=15",
|
|
25
|
+
"scipy>=1.10",
|
|
26
|
+
"simpleeval>=1.0",
|
|
27
|
+
"transitions>=0.9",
|
|
28
|
+
"typer>=0.27.3",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
plot = ["matplotlib>=3.7"]
|
|
33
|
+
# ty is pinned exactly: it is in beta (0.0.x) and its rules change between releases
|
|
34
|
+
dev = ["pytest>=7", "matplotlib>=3.7", "ruff>=0.6", "ty==0.0.89"]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
rigbox = "rigbox.cli:cli"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.package-data]
|
|
43
|
+
"rigbox.plugins.code" = ["macros/**/*.j2", "templates/**/*.j2"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
|
|
48
|
+
[tool.ty.environment]
|
|
49
|
+
python-version = "3.11"
|
|
50
|
+
|
|
51
|
+
[tool.ty.src]
|
|
52
|
+
include = ["src", "tests"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
target-version = "py311"
|
|
56
|
+
line-length = 160
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
# E/F: pycodestyle and pyflakes; I: import order; UP: modern syntax; B: likely bugs;
|
|
60
|
+
# D: docstrings (Google style); ANN: type annotations
|
|
61
|
+
select = ["E", "F", "I", "UP", "B", "D", "ANN"]
|
|
62
|
+
ignore = [
|
|
63
|
+
"B008", # Do not perform function call `typer.Argument` in argument defaults
|
|
64
|
+
"ANN401", # Dynamically typed expressions are disallowed
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint.pydocstyle]
|
|
68
|
+
convention = "google"
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint.per-file-ignores]
|
|
71
|
+
"tests/**" = ["D"] # tests are annotated like other code, but test functions don't need docstrings
|
rigbox-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Main package for Rigbox."""
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Main CLI entrypoint for Rigbox."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from rigbox.constants import RenderFormat, RenderLevel
|
|
11
|
+
from rigbox.core.context import Context
|
|
12
|
+
from rigbox.core.plugins import load_all
|
|
13
|
+
from rigbox.core.registry import CommandError, Registry, UsageError
|
|
14
|
+
from rigbox.core.render import RenderError, RigboxCmdResult
|
|
15
|
+
from rigbox.core.workspace import Workspace
|
|
16
|
+
|
|
17
|
+
cli = typer.Typer()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _build_context(workspace: str | None, fmt: RenderFormat) -> Context:
|
|
21
|
+
"""Find the workspace (from `workspace` or the cwd), load plugins and return a ready Context."""
|
|
22
|
+
ws = Workspace.find(Path(workspace) if workspace else Path.cwd())
|
|
23
|
+
registry = Registry()
|
|
24
|
+
load_all(registry, ws)
|
|
25
|
+
return Context(registry=registry, workspace=ws, fmt=fmt)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@cli.callback(invoke_without_command=True)
|
|
29
|
+
def main(ctx: typer.Context) -> None:
|
|
30
|
+
"""Main entrypoint for the Rigbox CLI. Overridden to allow for REPL launch when no arguments are provided.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
ctx (typer.Context): The Typer context object for the CLI invocation.
|
|
34
|
+
"""
|
|
35
|
+
# If no subcommand was invoked, we default to spawning the interactive REPL interface
|
|
36
|
+
if ctx.invoked_subcommand is None:
|
|
37
|
+
from rigbox.repl import run_repl
|
|
38
|
+
|
|
39
|
+
rigbox_ctx = _build_context(None, RenderFormat.default())
|
|
40
|
+
run_repl(rigbox_ctx)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@cli.command(help="Initialize a new Rigbox workspace.")
|
|
44
|
+
def init(
|
|
45
|
+
path: Path = typer.Argument(Path("."), help="The path to create the workspace in."),
|
|
46
|
+
name: str | None = typer.Option(None, "--name", help="The name of the workspace."),
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Initialize a new Rigbox workspace.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
path (Path, optional): The path to create the workspace in. Defaults to the current directory.
|
|
52
|
+
name (str | None, optional): The name of the workspace. Defaults to None.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
typer.Exit(code=1): Raised if the workspace already exists.
|
|
56
|
+
typer.Exit(code=0): Raised after successful initialization.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
ws = Workspace.init(path, name)
|
|
60
|
+
except FileExistsError as e:
|
|
61
|
+
typer.echo(f"Error: {e}")
|
|
62
|
+
raise typer.Exit(code=1) from e
|
|
63
|
+
typer.echo(f"Workspace initialized at {ws.root}")
|
|
64
|
+
raise typer.Exit(code=0)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@cli.command(context_settings={"ignore_unknown_options": True, "allow_interspersed_args": False}, help="Run one command non-interactively.")
|
|
68
|
+
def run(
|
|
69
|
+
tokens: Annotated[list[str], typer.Argument(help="<command> [args...]")],
|
|
70
|
+
workspace: Annotated[str | None, typer.Option(help="Start the workspace search here (default: cwd)")] = None,
|
|
71
|
+
fmt: Annotated[RenderFormat, typer.Option("--format", "-f", help=f"Output format (default: {RenderFormat.default()})")] = RenderFormat.default(),
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Run one command non-interactively.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
tokens (Annotated[list[str], typer.Argument): The command and its arguments to run.
|
|
77
|
+
workspace (str | None, optional): The workspace to start the search from. Defaults to cwd.
|
|
78
|
+
fmt: The output format, including plain text, tables, JSON, and NDJSON. Defaults to "auto".
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
typer.Exit(code=2): Raised if no command is specified.
|
|
82
|
+
typer.Exit(code=2): Raised if there is a usage error.
|
|
83
|
+
typer.Exit(code=1): Raised if there is a command error.
|
|
84
|
+
typer.Exit(code=0): Raised after successful execution.
|
|
85
|
+
"""
|
|
86
|
+
if not tokens:
|
|
87
|
+
typer.echo("Error: No command specified.")
|
|
88
|
+
raise typer.Exit(code=2)
|
|
89
|
+
ctx = _build_context(workspace, fmt)
|
|
90
|
+
try:
|
|
91
|
+
response = ctx.run_tokens(tokens)
|
|
92
|
+
out = response.render(fmt)
|
|
93
|
+
except UsageError as e:
|
|
94
|
+
raise typer.Exit(code=e.exit_code) from e
|
|
95
|
+
except (CommandError, RenderError) as e:
|
|
96
|
+
typer.echo(RigboxCmdResult(text=str(e), level=RenderLevel.ERROR).render(RenderFormat.PLAINTEXT), err=True)
|
|
97
|
+
raise typer.Exit(code=1) from e
|
|
98
|
+
finally:
|
|
99
|
+
ctx.close()
|
|
100
|
+
if out:
|
|
101
|
+
typer.echo(out)
|
|
102
|
+
raise typer.Exit(code=0)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@cli.command(help="Execute a script file within the Rigbox workspace.")
|
|
106
|
+
def script(
|
|
107
|
+
file: Annotated[Path, typer.Argument(help="The script file to execute.")],
|
|
108
|
+
workspace: Annotated[str | None, typer.Option(help="Start the workspace search here (default: cwd)")] = None,
|
|
109
|
+
fmt: Annotated[RenderFormat, typer.Option("--format", "-f", help=f"Output format (default: {RenderFormat.default()})")] = RenderFormat.default(),
|
|
110
|
+
var: Annotated[list[str] | None, typer.Option("--var", "-v", help="key=value override")] = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Execute a script file within the Rigbox workspace.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
file (Annotated[Path, typer.Argument): The script file to execute.
|
|
116
|
+
workspace (str | None, optional): The workspace to start the search from. Defaults to cwd.
|
|
117
|
+
fmt: The output format, including plain text, rich renderables, JSON, and NDJSON. Defaults to "auto".
|
|
118
|
+
var (Annotated[list[str] | None, typer.Option, optional): Key=value overrides for the script. Defaults to None.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
typer.Exit(code=2): Raised if there is a usage error.
|
|
122
|
+
typer.Exit(code=1): Raised if there is a command error.
|
|
123
|
+
typer.Exit(code=0): Raised after successful execution.
|
|
124
|
+
"""
|
|
125
|
+
ctx = _build_context(workspace, fmt)
|
|
126
|
+
try:
|
|
127
|
+
response = ctx.run("workflow.run", {"workflow": str(file), "var": var or []})
|
|
128
|
+
out = response.render(fmt)
|
|
129
|
+
except UsageError as e:
|
|
130
|
+
raise typer.Exit(code=e.exit_code) from e
|
|
131
|
+
except (CommandError, RenderError) as e:
|
|
132
|
+
typer.echo(RigboxCmdResult(text=str(e), level=RenderLevel.ERROR).render(RenderFormat.PLAINTEXT), err=True)
|
|
133
|
+
raise typer.Exit(code=1) from e
|
|
134
|
+
finally:
|
|
135
|
+
ctx.close()
|
|
136
|
+
if out:
|
|
137
|
+
typer.echo(out)
|
|
138
|
+
raise typer.Exit(code=0)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
cli()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Common constants for Rigbox."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from importlib.metadata import version
|
|
7
|
+
from typing import Final, Self
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AppMetadata:
|
|
11
|
+
"""Namespace class for application metadata-related constants."""
|
|
12
|
+
|
|
13
|
+
NAME: Final[str] = "rigbox"
|
|
14
|
+
VERSION: Final[str] = version(NAME)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AppFiles:
|
|
18
|
+
"""Namespace class for file-related constants."""
|
|
19
|
+
|
|
20
|
+
SHELL_HISTORY: Final[str] = "shell_history.json"
|
|
21
|
+
WORKSPACE_CONFIG: Final[str] = "rigbox.yaml"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RenderFormat(StrEnum):
|
|
25
|
+
"""Enumeration of output rendering formats.
|
|
26
|
+
|
|
27
|
+
This dictates how the output should be formatted when rendered, including options for JSON, NDJSON, Rich text, plain text,
|
|
28
|
+
or automatic detection based on the terminal capabilities.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
JSON = "json"
|
|
32
|
+
NDJSON = "ndjson"
|
|
33
|
+
RICH = "rich"
|
|
34
|
+
PLAINTEXT = "plaintext"
|
|
35
|
+
AUTO = "auto"
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def default(cls) -> Self:
|
|
39
|
+
"""Get the default rendering format."""
|
|
40
|
+
return cls.AUTO
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RenderLevel(StrEnum):
|
|
44
|
+
"""Enumeration of output rendering severity levels.
|
|
45
|
+
|
|
46
|
+
This dictates how messages of different severity levels should be rendered, including their associated colors.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
ERROR = "error"
|
|
50
|
+
WARNING = "warning"
|
|
51
|
+
INFO = "info"
|
|
52
|
+
SUCCESS = "success"
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def default(cls) -> Self:
|
|
56
|
+
"""Get the default rendering level."""
|
|
57
|
+
return cls.INFO
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def color(self) -> str:
|
|
61
|
+
"""Get the associated color for this `RenderLevel`.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
KeyError: If the `RenderLevel` does not have an associated color mapping.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
str: The rich color associated with this `RenderLevel`.
|
|
68
|
+
"""
|
|
69
|
+
return {
|
|
70
|
+
"error": "bold red",
|
|
71
|
+
"warning": "yellow",
|
|
72
|
+
"info": "default", # No color
|
|
73
|
+
"success": "green",
|
|
74
|
+
}[self]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Plugins:
|
|
78
|
+
"""Namespace class for plugin-related constants."""
|
|
79
|
+
|
|
80
|
+
PROJECT: Final[str] = AppMetadata.NAME
|
|
81
|
+
ENTRYPOINT_GROUP: Final[str] = f"{PROJECT}.plugins".lower()
|
|
82
|
+
BUILTIN_MODULES: tuple[str, ...] = (
|
|
83
|
+
f"{ENTRYPOINT_GROUP}.builtin",
|
|
84
|
+
f"{ENTRYPOINT_GROUP}.manifests.commands",
|
|
85
|
+
f"{ENTRYPOINT_GROUP}.device.commands",
|
|
86
|
+
f"{ENTRYPOINT_GROUP}.statemachine.commands",
|
|
87
|
+
f"{ENTRYPOINT_GROUP}.code.commands",
|
|
88
|
+
f"{ENTRYPOINT_GROUP}.dsp.commands",
|
|
89
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Framework core: registry, context, parsing, rendering, workspace, workflows and plugin loading."""
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Module for command-line parsing using pydantic models.
|
|
2
|
+
|
|
3
|
+
Rigbox defines all commands using pydantic models to represent their arguments.
|
|
4
|
+
|
|
5
|
+
This module provides functions to convert these pydantic models into argparse parsers,
|
|
6
|
+
so that commands can be run both from the command line and the interactive shell with consistent argument handling.
|
|
7
|
+
|
|
8
|
+
Required fields in the pydantic model become positional arguments, while optional fields become named options with `--`.
|
|
9
|
+
Boolean fields become bare flags, list fields can repeat, and Literal fields become validated choices.
|
|
10
|
+
|
|
11
|
+
Values stay as strings when parsed from the command line, and pydantic handles conversion and validation when the command runs.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import functools
|
|
18
|
+
import shlex
|
|
19
|
+
import typing
|
|
20
|
+
from types import UnionType
|
|
21
|
+
from typing import Any, NoReturn
|
|
22
|
+
|
|
23
|
+
import cmd2
|
|
24
|
+
from pydantic import BaseModel, Field
|
|
25
|
+
from pydantic.fields import FieldInfo
|
|
26
|
+
|
|
27
|
+
from rigbox.core.registry import Command, UsageError
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def path_field(default: Any = ..., **kwargs: Any) -> Any: # noqa: ANN401 - mirrors pydantic's Field()
|
|
31
|
+
"""Declare a ``str`` argument that holds a file path, so the shell can tab-complete file names for it.
|
|
32
|
+
|
|
33
|
+
This exists because some fields may represent both a file path
|
|
34
|
+
and other types of string data, and we want the shell to provide
|
|
35
|
+
tab-completion for the file path case.
|
|
36
|
+
|
|
37
|
+
Takes the same arguments as pydantic's ``Field``; with no default the argument is required.
|
|
38
|
+
"""
|
|
39
|
+
return Field(default, json_schema_extra={"path": True}, **kwargs)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RigboxParser(cmd2.Cmd2ArgumentParser):
|
|
43
|
+
"""The parser for one command. It remembers the last error so non-interactive callers can report it."""
|
|
44
|
+
|
|
45
|
+
fields: tuple[str, ...] = ()
|
|
46
|
+
"""Names of the args model's fields, used to pick them out of the parsed namespace."""
|
|
47
|
+
|
|
48
|
+
error_message: str = ""
|
|
49
|
+
|
|
50
|
+
def error(self, message: str) -> NoReturn:
|
|
51
|
+
"""Record `message`, then let argparse print the usage and exit as usual."""
|
|
52
|
+
self.error_message = message
|
|
53
|
+
super().error(message)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _unwrap(annotation: object) -> object:
|
|
57
|
+
"""Strip Optional[...] so `bool | None` still counts as bool."""
|
|
58
|
+
if typing.get_origin(annotation) in (typing.Union, UnionType):
|
|
59
|
+
args = [a for a in typing.get_args(annotation) if a is not type(None)]
|
|
60
|
+
if len(args) == 1:
|
|
61
|
+
return args[0]
|
|
62
|
+
return annotation
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_bool(info: FieldInfo) -> bool:
|
|
66
|
+
return _unwrap(info.annotation) is bool
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_list(info: FieldInfo) -> bool:
|
|
70
|
+
return typing.get_origin(_unwrap(info.annotation)) in (list, tuple, set)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_path(info: FieldInfo) -> bool:
|
|
74
|
+
extra = info.json_schema_extra
|
|
75
|
+
return isinstance(extra, dict) and extra.get("path") is True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _help_text(info: FieldInfo) -> str:
|
|
79
|
+
"""The field's description, with its default appended; `%` is doubled because argparse formats help text."""
|
|
80
|
+
text = info.description or ""
|
|
81
|
+
if info.default_factory is None and info.default not in (None, "", False) and not info.is_required():
|
|
82
|
+
text = f"{text} (default: {info.default})".strip()
|
|
83
|
+
return text.replace("%", "%%")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_parser(name: str, model: type[BaseModel], summary: str) -> RigboxParser:
|
|
87
|
+
"""Build the parser for command `name` from its args `model`; `summary` is shown as the description."""
|
|
88
|
+
# First, create a parser instance
|
|
89
|
+
parser = RigboxParser(prog=name, description=summary, allow_abbrev=False, suggest_on_error=True)
|
|
90
|
+
# Then, create the fields using the pydantic model's field definitions
|
|
91
|
+
parser.fields = tuple(model.model_fields)
|
|
92
|
+
# Now, we add each field as an argument to the parser based on its properties
|
|
93
|
+
for field, info in model.model_fields.items():
|
|
94
|
+
annotation = _unwrap(info.annotation)
|
|
95
|
+
options: dict[str, Any] = {"help": _help_text(info)}
|
|
96
|
+
if typing.get_origin(annotation) is typing.Literal:
|
|
97
|
+
options["choices"] = list(typing.get_args(annotation))
|
|
98
|
+
# Automatically set up path completion for path fields
|
|
99
|
+
if _is_path(info):
|
|
100
|
+
options["completer"] = cmd2.Cmd.path_complete
|
|
101
|
+
if info.is_required():
|
|
102
|
+
parser.add_argument(field, **options)
|
|
103
|
+
continue
|
|
104
|
+
flag = "--" + field.replace("_", "-")
|
|
105
|
+
if _is_bool(info):
|
|
106
|
+
parser.add_argument(flag, dest=field, action="store_true", default=argparse.SUPPRESS, help=options["help"])
|
|
107
|
+
elif _is_list(info):
|
|
108
|
+
parser.add_argument(flag, dest=field, action="append", default=argparse.SUPPRESS, **options)
|
|
109
|
+
else:
|
|
110
|
+
parser.add_argument(flag, dest=field, default=argparse.SUPPRESS, **options)
|
|
111
|
+
return parser
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@functools.cache # Cache the parser for each command to avoid rebuilding it multiple times
|
|
115
|
+
def parser_for(command: Command) -> RigboxParser:
|
|
116
|
+
"""Return the (cached) parser for a registered command."""
|
|
117
|
+
return build_parser(command.name, command.args_model, command.help)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def parse_args(parser: RigboxParser, tokens: list[str]) -> dict[str, Any]:
|
|
121
|
+
"""Parse command-line `tokens` into a raw args dict holding only the options actually given.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
UsageError: If the tokens are invalid, or ``--help`` was given. The parser has already printed the usage
|
|
125
|
+
message or help text; the error's ``exit_code`` is 2 for a bad command line and 0 for help.
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
namespace = parser.parse_args(tokens)
|
|
129
|
+
except SystemExit as exit_:
|
|
130
|
+
code = exit_.code
|
|
131
|
+
raise UsageError(parser.error_message, code if isinstance(code, int) else 0) from None
|
|
132
|
+
return {key: value for key, value in vars(namespace).items() if key in parser.fields}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def split_line(line: str) -> list[str]:
|
|
136
|
+
"""Split a line into tokens. Quotes group words; backslashes are kept (Windows paths)."""
|
|
137
|
+
lexer = shlex.shlex(line, posix=False)
|
|
138
|
+
lexer.whitespace_split = True
|
|
139
|
+
lexer.commenters = ""
|
|
140
|
+
tokens: list[str] = []
|
|
141
|
+
for tok in lexer:
|
|
142
|
+
if len(tok) >= 2 and tok[0] == tok[-1] and tok[0] in "\"'":
|
|
143
|
+
tok = tok[1:-1]
|
|
144
|
+
tokens.append(tok)
|
|
145
|
+
return tokens
|