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.
Files changed (83) hide show
  1. rigbox-0.1.0/LICENSE +21 -0
  2. rigbox-0.1.0/PKG-INFO +99 -0
  3. rigbox-0.1.0/README.md +68 -0
  4. rigbox-0.1.0/pyproject.toml +71 -0
  5. rigbox-0.1.0/setup.cfg +4 -0
  6. rigbox-0.1.0/src/rigbox/__init__.py +1 -0
  7. rigbox-0.1.0/src/rigbox/cli.py +142 -0
  8. rigbox-0.1.0/src/rigbox/constants.py +89 -0
  9. rigbox-0.1.0/src/rigbox/core/__init__.py +1 -0
  10. rigbox-0.1.0/src/rigbox/core/cli_parser.py +145 -0
  11. rigbox-0.1.0/src/rigbox/core/context.py +129 -0
  12. rigbox-0.1.0/src/rigbox/core/history.py +44 -0
  13. rigbox-0.1.0/src/rigbox/core/plugins.py +76 -0
  14. rigbox-0.1.0/src/rigbox/core/registry.py +191 -0
  15. rigbox-0.1.0/src/rigbox/core/render.py +253 -0
  16. rigbox-0.1.0/src/rigbox/core/transport.py +78 -0
  17. rigbox-0.1.0/src/rigbox/core/workflow.py +94 -0
  18. rigbox-0.1.0/src/rigbox/core/workspace.py +96 -0
  19. rigbox-0.1.0/src/rigbox/hooks.py +30 -0
  20. rigbox-0.1.0/src/rigbox/plugins/__init__.py +1 -0
  21. rigbox-0.1.0/src/rigbox/plugins/builtin.py +130 -0
  22. rigbox-0.1.0/src/rigbox/plugins/code/__init__.py +1 -0
  23. rigbox-0.1.0/src/rigbox/plugins/code/commands.py +84 -0
  24. rigbox-0.1.0/src/rigbox/plugins/code/generator.py +154 -0
  25. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/__init__.py +0 -0
  26. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_autogen_banner.j2 +27 -0
  27. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_autogen_banner_editable.j2 +27 -0
  28. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_autogen_banner_state_machine.j2 +29 -0
  29. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_enum.j2 +10 -0
  30. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_actions.j2 +7 -0
  31. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_constants.j2 +5 -0
  32. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_errors.j2 +10 -0
  33. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_events.j2 +10 -0
  34. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_state_table_entry.j2 +20 -0
  35. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_state_transition_table.j2 +20 -0
  36. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_states.j2 +9 -0
  37. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_struct.j2 +6 -0
  38. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_machine_version.j2 +3 -0
  39. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_struct.j2 +8 -0
  40. rigbox-0.1.0/src/rigbox/plugins/code/macros/c/c_union.j2 +8 -0
  41. rigbox-0.1.0/src/rigbox/plugins/code/render.py +106 -0
  42. rigbox-0.1.0/src/rigbox/plugins/code/template.py +64 -0
  43. rigbox-0.1.0/src/rigbox/plugins/code/templates/c/state_machine.h.j2 +42 -0
  44. rigbox-0.1.0/src/rigbox/plugins/device/__init__.py +1 -0
  45. rigbox-0.1.0/src/rigbox/plugins/device/commands.py +236 -0
  46. rigbox-0.1.0/src/rigbox/plugins/device/connection.py +160 -0
  47. rigbox-0.1.0/src/rigbox/plugins/device/replay_transport.py +77 -0
  48. rigbox-0.1.0/src/rigbox/plugins/device/serial_transport.py +84 -0
  49. rigbox-0.1.0/src/rigbox/plugins/manifests/__init__.py +1 -0
  50. rigbox-0.1.0/src/rigbox/plugins/manifests/api.py +96 -0
  51. rigbox-0.1.0/src/rigbox/plugins/manifests/build.py +98 -0
  52. rigbox-0.1.0/src/rigbox/plugins/manifests/check.py +79 -0
  53. rigbox-0.1.0/src/rigbox/plugins/manifests/commands.py +98 -0
  54. rigbox-0.1.0/src/rigbox/plugins/manifests/schema.py +130 -0
  55. rigbox-0.1.0/src/rigbox/plugins/plugin_template.py +43 -0
  56. rigbox-0.1.0/src/rigbox/plugins/statemachine/__init__.py +1 -0
  57. rigbox-0.1.0/src/rigbox/plugins/statemachine/check.py +169 -0
  58. rigbox-0.1.0/src/rigbox/plugins/statemachine/commands.py +357 -0
  59. rigbox-0.1.0/src/rigbox/plugins/statemachine/coverage.py +156 -0
  60. rigbox-0.1.0/src/rigbox/plugins/statemachine/engine.py +186 -0
  61. rigbox-0.1.0/src/rigbox/plugins/statemachine/fuzz.py +181 -0
  62. rigbox-0.1.0/src/rigbox/plugins/statemachine/schema.py +241 -0
  63. rigbox-0.1.0/src/rigbox/repl.py +135 -0
  64. rigbox-0.1.0/src/rigbox.egg-info/PKG-INFO +99 -0
  65. rigbox-0.1.0/src/rigbox.egg-info/SOURCES.txt +81 -0
  66. rigbox-0.1.0/src/rigbox.egg-info/dependency_links.txt +1 -0
  67. rigbox-0.1.0/src/rigbox.egg-info/entry_points.txt +2 -0
  68. rigbox-0.1.0/src/rigbox.egg-info/requires.txt +23 -0
  69. rigbox-0.1.0/src/rigbox.egg-info/top_level.txt +1 -0
  70. rigbox-0.1.0/tests/test_cli_parser.py +217 -0
  71. rigbox-0.1.0/tests/test_codegen.py +180 -0
  72. rigbox-0.1.0/tests/test_core.py +263 -0
  73. rigbox-0.1.0/tests/test_device.py +167 -0
  74. rigbox-0.1.0/tests/test_device_session.py +350 -0
  75. rigbox-0.1.0/tests/test_dsp.py +436 -0
  76. rigbox-0.1.0/tests/test_manifests.py +157 -0
  77. rigbox-0.1.0/tests/test_plugin_new.py +66 -0
  78. rigbox-0.1.0/tests/test_render.py +138 -0
  79. rigbox-0.1.0/tests/test_shell.py +150 -0
  80. rigbox-0.1.0/tests/test_sm_checks.py +198 -0
  81. rigbox-0.1.0/tests/test_sm_fuzz.py +190 -0
  82. rigbox-0.1.0/tests/test_statemachine.py +366 -0
  83. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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